-------
一、银行业务调度系统的项目需求
二、面向对象的分析与设计
对该系统的需求分析,至少可以得到三个对象:客户、取号机、业务窗口。
- 客户类。客户分为三种类型:普通客户、快速客户和VIP客户,所以很容易得出客户类用枚举来声明在合适不过。因为不涉及到对客户属性的调用,所以不必声明其他方法,该枚举很简单。
- 取号机。现在的银行业务应用中,都会用一台产生号码的机器来管理客户的业务顺序,只有被叫到的客户才会到业务窗口办理业务。该机器会产生三种类型的号码,这个是根据客户的类型来判断的。所以定义一个 NumberMachine 类来管理号码段,且一个银行中只有一台取号机,所以这个类被定义成单例模式(作用是实现多个线程的数据共享),这样保证了数据的号码唯一性,不会出现重号的情况。
- 业务窗口。需求中提供了6个业务窗口,分三种来对应处理三种类型客户的业务。实际应用中,业务窗口数是可调的,即三种类型业务窗口具有不确定性,快速窗口和VIP窗口也可能被分配为普通窗口,所以把对业务窗口的生成用判断语句来实现,这样可以随意更改窗口类型,一般业务窗口个数都是固定值。在窗口办理业务的时候,是通过叫号来完成。而叫号其实是在调用取号机上的正等待服务的号码,所以取号机上有一个产生号码的方法。
- 除上诉直观关对象外,对于生成新号码还有一个号码管理器来对其进行管理,这些号码都是正在等待服务的客户号码。学习视屏之后,根据面向对象的思想,可以发现生成新号码这个方法是在调用号码管理器中的属性,所以把号码生成的方法定义在该类中。
该系统类图:
三、程序分析
1、NumberMachine 和NumberManager 两个类是办理窗口业务的前提。
- 前面分析过取号机,需要被设计成单例,才能满足多线程的数据共享。而共享数据分为三类,分别是普通类型、快速类型和VIP类型,这三类号码都是号码管理器的一个实例。所以将这三类号码定义在取号机NumberMachine 中,保证它们的唯一性。该部分代码比较简单,重点在于能够想到通过单例设计来完成。
- 通过前面分析号码产生机制,可以发现,号码的生成可以用一个计时器来实现,这里多线程的访问方式是一个在产生(generateNewNumber)号码,一个在取出(fetchNumber)号码,所以这两个方法设计成同步函数。不同线程在调用这两个方法时,当调用其中的一个,另一个方法也会被锁住。号码的存储同交通管理系统中同 Road 类上的车的设计一样,存入集合中,都要求满足先进先出的原则(可以悬在LinkedList,这里换ArrayList ,那么我们只需移除0角标的元素即可)。
号码的生成我们通过计时器来完成,交通管理系统中有详细讲解。号码生成时间比为:VIP客户:普通客户:快速客户 = 1:6:3,若1s生成一个普通客户,那么6s生成一个VIP客户,2s生成一个快速客户。我们的计时器也按照这个时间频率来设置。代码如下:
public void getCustomerNumber(final CustomerType type){ Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate( new Runnable(){ @Override public void run() { Integer serviceNumber = generateNewNumber(); System.out.println("第" + serviceNumber + "号" + type.toString() + "客户正在等待服务..."); } }, 0, getPeriod(type), TimeUnit.SECONDS); } //通过类型判断来传入不同的时间频率 private long getPeriod(CustomerType type){ long period = 2147483647; long commonTime = Constants.COMMON_CUSTOMER_INTERVIEW_TIME; switch (type) { case COMMON: period = commonTime; break; case EXPRESS: period = commonTime * 2; break; case VIP: period = commonTime * 6; break; } return period; }
为了提供代码复用性,定义了getPeriod 方法来传递时间频率,这样只需定义一个计时器就可以对三种类型号码进行操作了。这里涉及到了常量的应用,需求中也有说明,即办理业务的最大值和最小值,还有普通客户的的产生频率,若为了体现的更真实,可以将普通客户的产生频率定义为一个随机值,其它两类客户产生频率也随这个随机值变化。这里为了方便,将普通客户的产生频率固定在1s一个。关于常量值的定义也可以通过配置文件的方式来传入,这样操作会更简单,不会涉及到源码。
2、业务窗口类 ServiceWindow
该系统设计的第二个难点,关于三种类型窗口的定义。需求中一共有6个窗口,也就是6个线程,该6个线程对普通号码都是共享的。
一个ServiceWindow 对象即一个线程,首先得定义一个开启线程的方法。这里也用到了线程池工具类 Executors,查阅API 可得在 Executor 接口中有一个开启线程的方法 execute(Runnable command),使用其子接口ScheduledExecutorService 来定义该窗口类型。
public void start(){
Executors.newSingleThreadScheduledExecutor().execute(
new Runnable() {
public void run() {
while (true) {
if(type.name()=="COMMON")
commonWindow();
else
serviceWindow();
}
}
});
}
下面是处理具体的办理类型的声明,这个细节难度很大,容易出错。这块代码涉及到了所有定义过的类,容易搞糊涂,所以主要是弄清细节。原理其实很简单,客户到对应业务窗口办理相关业务,办理业务时间有最小到最大这么一个随机值,业务事件通过sleep 方法来完成。然后就是一个在测试类中循环调用的过程,所以原理简单,但是细节麻烦。先把普通窗口的处理声明写好,方法声明如下:
private void commonWindow(){
String windowName = windowID + "号" + type + "窗口";
System.out.println(windowName + "正在获取普通任务...");
Integer serviceNumber = NumberMachine.getInstance().getCommonManager().fetchNumber();
if (serviceNumber != null) {
System.out.println(windowName + "开始为" + serviceNumber + "号普通客户服务...");
int serviceTime = new Random().nextInt(Constants.MAX_SERVICE_TIME) + 1;
try {
Thread.sleep(serviceTime * 1000);
} catch (InterruptedException e) {
throws new RuntimeException(e);
}
System.out.println(windowName + "完成为" + serviceNumber + "号普通客户服务,耗时" + serviceTime + "秒!");
} else {
System.out.println(windowName + "没有取到普通任务,休息1秒。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throws new RuntimeException(e);
}
}
}
快速和VIP窗口的声明就是普通型的一个子方式,只需在客户类型上做哈修改就满足条件。需求中还要求快速和VIP窗口也能处理普通业务,所以这两个业务最后需要调用普通业务的方法。快速和VIP业务都属于特殊业务,所以可以通过抽取代码通过客户类型来判断办理业务的类型,这样提高代码的复用性。代法如下:
//随机服务类型窗口,根据服务类型变更。
private void serviceWindow(){
String windowName = windowID + "号" + type + "窗口";
System.out.println(windowName + "正在获取" + type + "任务...");
Integer serviceNumber = getServiceNumber();
if (serviceNumber != null) {
System.out.println(windowName + "开始为" + serviceNumber + "号" + type + "客户服务...");
int serviceTime = getServiceTime();
try {
Thread.sleep(serviceTime * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(windowName + "完成为" + serviceNumber + "号" + type + "客户服务,耗时" + serviceTime + "秒!");
} else {
commonWindow();
}
}
/*根据服务类型(客户类型)获取取号机生成的对应类型的号码*/
private Integer getServiceNumber(){
Integer serviceNumber = null;
switch (type) {
case EXPRESS:
serviceNumber = NumberMachine.getInstance().getExpressManager().fetchNumber();
break;
case VIP:
serviceNumber = NumberMachine.getInstance().getVIPManager().fetchNumber();
break;
default:
break;
}
return serviceNumber;
}
/*三种类型的客户办理业务的随机时间不同,但快速客户耗时最少(设置为1s)*/
private int getServiceTime(){
int serviceTime;
if (type == CustomerType.VIP) {
serviceTime = new Random().nextInt(Constants.MAX_SERVICE_TIME) + 1;
} else {
serviceTime = Constants.MIN_SERVICE_TIME;
}
return serviceTime;
}
四、总结
交通灯系统和银行系统的对比:
- 相似点:都涉及到集合的应用、枚举的定义和计时器技术。
- 着重点:交通灯处理的重点是关于 Road 类的简化分析,枚举元素的定义方式和计时器的应用。
银行系统的着重点针对客户类型而产生的三种类型的业务号码,计时器的应用以及多线程技术的安全问题。 - 互比性:交通灯系统处理都是单线程,所以不用考虑安全性。但银行系统用到了多线程,这时就需要考虑安全性,多线程的问题也是交通灯中所没有遇到的,我们在对数据进行加锁时,需要找准需要被锁定的数据,即明确哪些数据是大家共享的。
- 面向对象:对需求的分析,得把握住所有必须的对象,同时抓住隐藏的对象,这点最难,这个不是一蹴而就的,需要在实际的开发中慢慢积累,见的多了,就会了。就像张老师在所说的,如果没有这方面的经验,他也不知道用这样的方式来进行设计。所以先得多理解前辈的设计思想,在将其应用,然后转换成自己的设计思想。面向对象的分析,记住一点:谁拥有数据,谁就是对外提供操作这些数据的方法。