------- android培训、java培训、期待与您交流! ----------
银行业务调度系统
说明:
看完张老师的两个7k系统视频之后,我对面向对象设计的思想的认识更深刻了,对科技公司的项目的了解也是从无到有.我以前的潜意识是系统是一项很庞大的工程,一种很复杂,一种需要很多人配合很长时间才能完成的工作,
总而言之,我以前的想法就是,公司的系统一个人是完成不了的,直到看了张老师的视频我才知道,原来系统也不是我想象中的那么深不可及,张老师幽默风趣的讲解很是透彻.张老师虽然英年早逝,但他给java学子们留下了宝贵的财富.在这里我对张老师表示由衷的敬佩!
另:本篇博客中大部分还是借用了张老师的代码,并加上我的理解以及注释以及一些笔记,我对serviceWindow这个类进行了比较多的修改.实现不同类型的窗口调用了统一的方法达到需求所需的效果.
业务需求:模拟实现银行业务调度系统逻辑,具体需求如下:
- 银行内有6个业务窗口,1 - 4号窗口为普通窗口,5号窗口为快速窗口,6号窗口为VIP窗口。
- 有三种对应类型的客户:VIP客户,普通客户,快速客户(办理如交水电费、电话费之类业务的客户)
- 异步随机生成各种类型的客户,生成各类型用户的概率比例为: VIP客户 :普通客户 :快速客户 = 1 :6 :3
- 客户办理业务所需时间有最大值和最小值,在该范围内随机设定每个VIP客户以及普通客户办理业务所需的时间,快速客户办理业务所需时间为最小值(提示:办理业务的过程可通过线程Sleep的方式模拟)。
- 各类型客户在其对应窗口按顺序依次办理业务。
- 当VIP(6号)窗口和快速业务(5号)窗口没有客户等待办理业务的时候,这两个窗口可以处理普通客户的业务,而一旦有对应的客户等待办理业务的时候,则优先处理对应客户的业务。
- 随机生成客户时间间隔以及业务办理时间最大值和最小值自定,可以设置。
- 不要求实现GUI,只考虑系统逻辑实现,可通过Log方式展现程序运行结果
面向对象的分析与设计:
- 我们都去过银行,一般我们进去服务人要我们去取号机上取号,取号后等待被窗口叫到你的号码才能办理业务.下面我们根据需要逐步分析.
- 业务窗口:普通,快速,VIP,这里我们可以提取到的信息,应该有一个窗口类,
- 提供三种类型的服务,有三种类型的客户,就要有三种号码管理器,所以每种客户的号码编排是相互独立的,而取号机只有一个.因此取号机要设置成单例模式
- 异步随机生成三类客户,且成比例:这里我们可以用线程实现,如何让他们成比例的生成客户?java线程有个新技术:叫线程池,可以按照固定频率执行目标代码
- 客户办理业务所需时间有最大值和最小值,vip和普通所花费的时间是min和max之间的,这里我们可以用随机数表示,并用sleep模拟
- 各类型客户在其对应窗口按顺序依次办理业务,说明我们需要被叫号.那么谁告诉窗口等待的号码呢?窗口的号码当然是由取号机告诉它
- 随机生成客户时间间隔以及业务办理时间最大值和最小值自定.说明我们可以设置为静态常量.
主要的类:
取号机:NumberMachine 方法:getCommonManager(),getExpressManager(),getVipManager(), getInstance()
号码管理器:generateNewNumber(),fetchNumber()
服务窗口:ServiceWindow方法:start(),commonService(),expressService(),vipService(),get/setWindowType(),get/setWindowID()
代码设计:NumberManager类
1.定义一个存储上一个号码的变量以获取当前号码
2.需要知道哪些人(号码)正在等待,所以定义一个集合用来存放产生的用户号码
3.定义一个产生新号码的方法和获取下一个要服务的号码的方法,这两个方法被不同的线程操作了相同的数据,所以,要进行同步
4获取下一个要服务的号码,并把这个号码从集合中移除.
import java.util.ArrayList;
import java.util.List;
public class NumberManager {
private int lastNumber = 0;
private List<Integer> queueNumbers = new ArrayList<Integer>();//定义集合用来存放正在等待的顾客
//用同步方法以保证线程安全
public synchronized Integer generateNewNumber(){
queueNumbers.add(++lastNumber);
return lastNumber;
}
public synchronized Integer fetchNumber(){
if(queueNumbers.size()>0){//如果有客户等待
return (Integer)queueNumbers.remove(0);//获取站在等待队列第一个位子的顾客,并把他从集合中移除
}else{
return null;
}
}
}
NumberMachine类
1创建三个私有的NumberManager对象,分别表示普通、快速和VIP客户的号码管理器
2.定义三个对应的方法来返回这三个NumberManager对象以获取三个对象。
3.将NumberMachine类设计成单例。
4私有化构造器,在本类私有化 NumberMachine类对象,提供获取NumberMachine对象的静态方法
/**
* 单例模式
* 1.不让外界new对象->私有化构造器
* 2.必须要有对象访问本类方法和属性->在本类中创建私有化对象
* 3.必须让外界获得本类对象,提供一个静态方法,返回本类对象
* 4.静态方法里面的属性必须是静态的-->让成员变量被static修饰
*/
private NumberMachine(){}
private static NumberMachine instance = new NumberMachine();
public static NumberMachine getInstance(){
return instance;
}
//创建三个NumberManger对象
private NumberManager commonManager = new NumberManager();
private NumberManager expressManager = new NumberManager();
private NumberManager vipManager = new NumberManager();
//分别提供获取获取对象的方法
public NumberManager getCommonManager() {
return commonManager;
}
public NumberManager getExpressManager() {
return expressManager;
}
public NumberManager getVipManager() {
return vipManager;
}
}
CustomerType枚举类
1.顾客的类型是确定的,当一个类的对象是有限而且固定的时候我们可以用定义一个枚举类.
2.定义三个对象分别表示三种类型的客户。
3.重写toString方法,返回类型的中文名称。
4.关于枚举:
何时用?:
- 一个类的对象是有限而且固定的,例如季节类,只能有 4 个对象
怎么用?:
- private 修饰构造器
- 属性使用 private final 修饰
- 把该类的所有实例都使用 public static final 来修饰(系统会自动添加 public static final 修饰)
枚举类和普通类的区别:
- 默认继承了 java.lang.Enum 类
- 构造器只能使用 private 访问控制符\
- 枚举类的所有实例必须在枚举类中显式列出(, 分隔 ; 结尾).
public enum CustomerType {
COMMON, EXPRESS, VIP;
//重写toString()方法
public String toString() {
String name = null;
switch (this) {
case COMMON:
name = "普通";
break;
case EXPRESS:
name = "快速";
break;
case VIP:
name = name();
break;
}
return name;
}
}
ServiceWindow类:
定义三个方法分别对三种客户进行服务,为了观察运行效果,应详细打印出其中的细节信息。
2.而我对这个类进行修改之后:在start内部定义一个线程,这个线程执行一个方法;这个方法判断当前调用窗口的类型在分别为为窗口初始化相应的一些参数,
然后提供一个统一的入口的方法,这个方法无论是什么窗口,执行之后都能后达到需求要求的效果
下面我们来看看这些代码:
public class ServiceWindow {
int maxRandom = Constants.MAX_SERVICE_TIME - Constants.MIN_SERVICE_TIME;
int serviceTime = new Random().nextInt(maxRandom)// 设置默认办理业务的时间
+ Constants.MIN_SERVICE_TIME;
int flag = 0; // 控制非普通窗口执行普通任务的标志
// 1.打开转换窗口类型代码的钥匙2.用来存储非普通"变"为普通窗口之前类型/ 3.阻止普通窗口的锁
CustomerType typeFlag = CustomerType.COMMON;
private CustomerType windowType;
private CustomerType customerType;
private int windowID = 1;
public void setWindowType(CustomerType windowType) {
this.windowType = windowType;
}
public void setWindowID(int windowID) {
this.windowID = windowID;
}
public void start() {
Executors.newSingleThreadExecutor().execute(new Runnable() {
public void run() {
while (true)
startService();
}
});
}
private void startService() {
Integer serviceNumber = null;
NumberMachine instance = NumberMachine.getInstance();// 获取取号机的实例
switch (windowType) {
case COMMON:// common入口
serviceNumber = instance.getCommonManager().fetchNumber();
if (typeFlag != CustomerType.COMMON) { // typeFlag中保存了非普通窗口的类型
windowType = typeFlag; // 如果是非普通窗口调用普通窗口的方法.就恢复窗口类型
typeFlag = CustomerType.COMMON; // typeFlag恢复为普通类型,让普通窗口不能执行当前代码块
}
customerType = CustomerType.COMMON;
break;
case EXPRESS:
serviceNumber = instance.getExpressManager().fetchNumber();
customerType = CustomerType.EXPRESS;
serviceTime = Constants.MIN_SERVICE_TIME;// 快速客户时间为最小值
break;
case VIP:
serviceNumber = instance.getVipManager().fetchNumber();
customerType = CustomerType.VIP;
break;
}
service(serviceNumber);// 统一出口
}
private void service(Integer serviceNumber) {
String windowName = "第[" + windowID + "]号[" + windowType + "]窗口";
System.out.println(windowName + "开始获取 [" + customerType + "]任务!");
if (serviceNumber != null) {
System.out.println(windowName + "开始为第[" + serviceNumber + "]号["
+ customerType + "]客户服务");
try {
Thread.sleep(serviceTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(windowName + "完成为第[" + serviceNumber + "]号["
+ customerType + "]客户服务,总共耗时[" + serviceTime / 1000 + "]秒");
} else {
System.out.print(windowName + "没有取到[" + customerType + "]任务! ");
if ((flag % 2 == 0) && (windowType != CustomerType.COMMON)) {// 双重控制,只有vip和快速才能执行,且只能执行一次
flag++; // 变为奇数,防止进入common的非普通窗口再次进入commom
typeFlag = windowType;
windowType = CustomerType.COMMON;
startService(); // 执行完这个方法,customerType就变成common了,所以加了下一条代码
customerType = null; // 防止下面if块代码执行两遍,(第一遍是在startService();里执行的)
}
if (customerType == CustomerType.COMMON) { // 如果是没有普通用户就暂停1s
System.out.println("正在空闲一秒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
flag = 0;// 无论非普通窗口找到或没有找到普通用户都让flag恢复初始值,以防止下一个非普通窗口因为flag=1而不能执行查找普通用户的代码
}
}
这个类的执行流程是这样的:
1.线程启动后就循环调用startService()方法,startService()方法判断调用窗口的类型 获取相应类型的取号机的队列号码,设置客户类型为窗口类型,设置不同类型客服的服务时间
2.调用service()方法
当一个窗口调用到service()方法,首先会根据窗口和客户的类型(刚开始类型是一样的)打印xx'窗口正在获取xx类型的任务,如果有正在等待的客户则执行为客户服务的代码块,并打印所花费的时间
if (serviceNumber != null) {
System.out.println(windowName + "开始为第 [" + serviceNumber + "] 号 ["
+ customerType + "] 客户服务");
try {
Thread.sleep(serviceTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(windowName + "完成为第 [" + serviceNumber + "] 号 ["
+ customerType + "] 客户服务,总共耗时 [" + serviceTime / 1000
+ "] 秒");
}
3.如果窗口没有获取到相应的客户类型,则执行无客户的代码块,首先系统会根据if ((flag % 2 == 0) && (windowType != CustomerType.COMMON)) 代码判断当窗口为快速或vip窗口(flag初始值为0),如果是普通窗口则跳过 .
else {
System.out.print(windowName + "没有取到 [" + customerType + "] 任务! ");
if ((flag % 2 == 0) && (windowType != CustomerType.COMMON)) {// 双重控制,只有vip和快速才能执行,且同一个窗口只能执行一次
flag++; // 变为奇数,防止进入common的非普通窗口再次进入commom
typeFlag = windowType;
windowType = CustomerType.COMMON;
startService(); // 执行完这个方法,customerType就变成common了,所以加了下一条代码
customerType = null; // 防止下面if块代码执行两遍,(第一遍是在startService();里执行的)
}
.....
}
4.对于非普通窗口,他们还要在没有客户的时候去获取普通任务,所以必须调用 startService()方法进入common窗口入口.因为vip和express窗口的类型不是common,如果直接调用startService()方法就会再次进入express或vip窗口入口而造成死循环.
private void startService() {
Integer serviceNumber = null;
NumberMachine instance = NumberMachine.getInstance();// 获取取号机的实例
switch (windowType) {
case COMMON:// common入口
serviceNumber = instance.getCommonManager().fetchNumber();
customerType = CustomerType.COMMON;
break;
case EXPRESS://express入口
serviceNumber = instance.getExpressManager().fetchNumber();
customerType = CustomerType.EXPRESS;
serviceTime = Constants.MIN_SERVICE_TIME;// 快速客户时间为最小值
break;
case VIP://vip入口
serviceNumber = instance.getVipManager().fetchNumber();
customerType = CustomerType.VIP;
break;
}
service(serviceNumber);//统一出口
}
5.所以我们必须在调用方法之前把窗口类型改为common,这时候我们又发现,以express为例,调用startService()后的的提示全变为了 "普通窗口正在获取普通任务"而不是我们想要的"快速窗口获取普通任务".所以我们想到进入common入口之后必须把窗口类型改回来,这时我们需要通过设置一个变量typeFlag;来存储改变之前的窗口类型.且在进入common入口之后把存储在变量的窗口类型恢复
windowTypewindowType = typeFlag;
6.但这样又引发了另一个问题,这样肯定会影响到原来的普通窗口的窗口类型,所以我们还得给这句代码设置一定的执行条件,我们想到了这个办法.
case COMMON:// common入口
serviceNumber = instance.getCommonManager().fetchNumber();
if (typeFlag != CustomerType.COMMON) { // typeFlag中保存了非普通窗口的类型
windowType = typeFlag; // 如果是非普通窗口调用普通窗口的方法.就恢复窗口类型
typeFlag = CustomerType.COMMON; // typeFlag恢复为普通类型,让普通窗口不能执行当前代码块
}
customerType = CustomerType.COMMON;
break;
8.非普通窗口开始按照普通窗口的模式获取普通任务.
9.无论种类型的窗口,在没有获取到普通任务时都要提示"获取不到普通任务,并休眠一段时间"
if (customerType == CustomerType.COMMON) { // 如果是没有普通用户就暂停1s
System.out.println("正在空闲一秒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
10.最后还的注意的是如果非普通窗口,他在没有获取到普通任务时已经执行过一次上面的提示了,在执行完startService();方法后,由于顾客类型已经变为common,所以会出现上面那段代码执行两次的情况,如何避免呢?我们只需在执行startService();下面加一句代码,这样就不会重复执行了!
if ((flag % 2 == 0) && (windowType != CustomerType.COMMON)) {// 双重控制,只有vip和快速才能执行,且只能执行一次
flag++; // 变为奇数,防止进入common的非普通窗口再次进入commom
typeFlag = windowType;
windowType = CustomerType.COMMON;
startService(); // 执行完这个方法,customerType就变成common了,所以加了下一条代码
customerType = null; // 防止下面if块代码执行两遍,(第一遍是在startService();里执行的)
MainClass类:
程序入口,主要负责创建窗口,并启动窗口线程中的start()方法,还需要提供一个不停的产生号码的方法,我们用线程模拟,这里用到了线程池的新技术.我们创建三个定时器,分别用来创建普通,快速,VIP三个类型的号码.
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MainClass {
public static void main(String[] args) {
//产生4个普通窗口,设置窗口的1,2,3,4,并设置窗口的类型为普通
for(int i=1;i<5;i++){
ServiceWindow window = new ServiceWindow();
window.setWindowID(i);
window.setWindowType(CustomerType.COMMON);
window.start();//启动线程
}
//产生1个快速窗口,设置窗口id为5,类型为快速,并启动线程
ServiceWindow expressWindow = new ServiceWindow();
expressWindow.setWindowType(CustomerType.EXPRESS);
expressWindow.setWindowID(5);
expressWindow.start();
//产生1个VIP窗口 ,设置窗口id为6,类型为VIP,并启动线程
ServiceWindow vipWindow = new ServiceWindow();
vipWindow.setWindowType(CustomerType.VIP);
vipWindow.setWindowID(6);
vipWindow.start();
//普通客户拿号
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(
new Runnable(){
public void run(){
Integer serviceNumber = NumberMachine.getInstance().getCommonManager().generateNewNumber();
System.err.println("第 [" + serviceNumber + "] 号 [普通] 客户正在等待服务!");
}
},
0,
Constants.COMMON_CUSTOMER_INTERVAL_TIME,
TimeUnit.SECONDS);
//快速客户拿号
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(
new Runnable(){
public void run(){
Integer serviceNumber = NumberMachine.getInstance().getExpressManager().generateNewNumber();
System.err.println("第 [" + serviceNumber + "] 号 [快速] 客户正在等待服务!");
}
},
0,
Constants.COMMON_CUSTOMER_INTERVAL_TIME * 2,
TimeUnit.SECONDS);
//VIP客户拿号
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(
new Runnable(){
public void run(){
Integer serviceNumber = NumberMachine.getInstance().getVipManager().generateNewNumber();
System.err.println("第 [" + serviceNumber + "] 号 [VIP] 客户正在等待服务!");
}
},
0,
Constants.COMMON_CUSTOMER_INTERVAL_TIME * 6,
TimeUnit.SECONDS);
}
}
Constants类
定义三个常量:MAX_SERVICE_TIME、MIN_SERVICE_TIME、COMMON_CUSTOMER_INTERVAL_TIME
public class Constants {
public static int MAX_SERVICE_TIME = 10000; //10秒!
public static int MIN_SERVICE_TIME = 1000; //1秒!
/*每个普通窗口服务一个客户的平均时间为5秒,一共有4个这样的窗口,也就是说银行的所有普通窗口合起来
* 平均1.25秒内可以服务完一个普通客户,再加上快速窗口和VIP窗口也可以服务普通客户,所以,
* 1秒钟产生一个普通客户比较合理,*/
public static int COMMON_CUSTOMER_INTERVAL_TIME = 1;
}
运行效果:
关于这个银行调度系统的代码,本人正在以另一种方式尝试修改:
定义三个客户类以及一个他们的父类,客户类中封装了id,serviceTime,CustomerType客户从取号机获取id,取号机提供取号的方法和获取正在等待客户的id,银行叫号获取客户的信息,根据客户类型再提供相应的服务.而seviceWindows类提供一个统一方法(利用java多态进行统一的处理和instance对类型进行判断)目前大部分已经实现,还有少量bug,因为考虑的提交技术的博客的紧急性(我已经错过2期了)所以先写张老师的版本吧,那个版本我也会利用其它时间写到博客上面的.
的的
------- android培训、java培训、期待与您交流! ----------