37.2 最佳实践

本章我们粗略地讲解了一个MVC框架。一个MVC框架要考虑的外界环境因素太多了, 而且本身MVC框架也是一个轻量型的,就是希望我们编写的程序在没有Struts、Spring MVC 等框架的环境中不需要大规模的修改照样能够运行,所以编写一个框架不是一件容易的事情。幸运的是我们以学习模式为主,通过设计MVC框架来了解设计模式。我们来看看本章用到了哪些模式。

  • 工厂方法模式:通过工厂方法模式把所有的拦截器链实现出来,方便在系统初始化时 直接处理。
  • 单例模式:Action的默认配置都是单例模式,在一般的应用中单例已经足够了,在复 杂情况下可以使用享元模式提供应用性能,减少单例模式的性能隐患。
  • 责任链模式:建立拦截器链以及过滤器链,实现任务的链条化处理。
  • 迭代器模式:非常方便地遍历拦截器链内的拦截器,而不用再自己写遍历拦截器链的 方法。
  • 中介者模式:以核心控制器为核心,其他同事类都负责为核心控制器“打工”,保证核 心控制器瘦小、稳定。
  • 观察者模式:配置文件修改时,不用重启应用可以即刻生效,提供使用者的体验。
  • 桥梁模式:使不同的视图配合不同的语言文件,为终端用户展示不同的界面。
  • 策略模式:对XML文件的检查可以使用两种不同的策略,而且可以在测试机和开发机 中使用不同的检查策略,方便系统间自由切换。
  • 访问者模式:在解析XML文件时,使用访问者非常方便地访问到需要的对象。
  • 适配器模式:把一个开发者不熟悉的对象转换为熟悉的对象,避免工具或框架对开发 者的影响。
  • 门面模式:Action分发器负责所有的Action的分发工作,它提供了一个调用Action的唯 一入口,避免外部模块深入到模型模块内部。
  • 代理模式:大量使用动态代理,确保了框架的智能化。

MVC框架有非常成熟的源码,有兴趣的读者可以看看Struts、Spring MVC等源码,其中 包含了非常多的设计模式。读源码是提高设计技能和开发技能的一个重要途径,看一本书是 与作者进行了一次心灵交互,看一份源码是与一群作者进行心灵交互,对提高自己的技术修 养有非常大的帮助。

38.2 对象池模式

上周二,师兄过来找我,他负责运维一个大型新闻网站,说是网站出现性能,让我帮忙分析调优。我这几天正好闲得手痒,同时又卖个人情,何乐而不为呢。于是我们俩就到机房蹲点,追查问题。

38.2.1 正确的池化

简单说明一下该系统的场景,这是一个专业的新闻追踪网站,关注的是专业新闻的深度,在行业内具有相当大的影响力。最近一段时间内出现偶发性缓慢,从监控情况上看,响应时间在2秒以上,由于最近软硬件环境都没有变更过,因此直觉判断:最快捷、直观的解决方案就是增加DB硬件设备。但由于东家是穷惯了,不同意在没有彻查问题之前而依靠增强硬件来解决问题,于是我们这些软件工程师就忙活起来了。

网站首页内容基本都是静态的(轮询生成),唯一的动态部分是网站的激励语,比如“积一时之跬步,臻千里之遥程”、“业精于勤,荒于嬉;行成于思,毁于随”等励志语句, 这是一个简单的SQL随机查询结果,表中的数量在5000条左右,而且结构简单,查询性能不是问题。示例代码如代码清单38-29所示。

代码清单38-29 无缓存的SQL随机读取

1
2
3
4
5
6
7
8
@Service
public class WisdomProvider {
@Autowire
private WisdomDao wisdomDao;
public String getOneWord() {
return wisdomDao.randomOneWisdom();
}
}

对于代码中的@Service、@Autowire注解,做过Spring开发的都懂,这是一个典型的三层架构,WisdomDao的randomOneWisdom方法是通过数据库随机函数查询一条记录。在跟踪过程中,发现高峰期数据库连接偶尔出现占满情况,而且都是查询该表(顺便说下,该数据库的随机查询算法有缺陷),问题找到了:每一次访问都会直接查询数据库,没有缓存。通常情况下,这没有问题,但是在高并发的情况下,例如在10万PV的压力下服务器基本就垮掉了,这是非常严重的问题。

怎么解决呢?好办,引入一个对象池,把这5000条记录(根据评估最多不超过20000条记录)在启动时直接加载到内存中,在需要时再从内存中取得,以后查询不再与数据库交互。示例代码如代码清单38-30所示。

代码清单38-30 增加缓存后的随机读取

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class WisdomProvider {
@Autowire
private WisdomDao wisdomDao;
private List<String> wisdoms = null;
@PostConstruct
public void init() {
wisdoms = wisdomDao.getAll();
}
public String getOneWord() {
return RandomUtils.getOne(wisdoms);
}
}

@PostConstruct注解的作用是Spring容器在启动完毕后,直接执行init方法,一次性读取 所有的数据,然后在应用运行期间不再与数据库交互,直接从List列表中获取数据。通过这 样的修正,系统性能有了大幅提升,在不增加硬件的情况下,彻底解决了性能问题。这就是 对象池模式。

38.2.2 对象池模式的意图

对象池模式,或者称为对象池服务,其意图如下:
通过循环使用对象,减少资源在初始化和释放时的昂贵损耗[^1]。

注意这里的“昂贵”可能是时间效益(如性能),也可能是空间效益(如并行处理),在大多的情况下,“昂贵”指性能。

简单地说,在需要时,从池中提取;不用时,放回池中,等待下一个请求。典型例子是连接池和线程池,这是我们开发中经常接触到的。类图如图38-6所示。

image-20211001233332624

图38-6 对象池模式通用类图

对象池提供两个公共的方法:checkOut负责从池中提取对象,checkIn负责把回收对象 (当然,很多时候checkIn已经自动化处理,不需要显式声明,如连接池),对象池代码如代码清单38-31所示。

代码清单38-31 对象池示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public abstract class ObjectPool<T> {
//容器,容纳对象
private Hashtable<T, ObjectStatus> pool = new Hashtable<T, ObjectStatus>();
//初始化时创建对象,并放入到池中
public ObjectPool() {
pool.put(create(), new ObjectStatus());
}
//从Hashtable中取出空闲元素
public synchronized T checkOut() {
//这是最简单的策略
for (T t : pool.keySet()) {
if (pool.get(t).validate()) {
pool.get(t).setUsing();
return t;
}
}
return null;
}
//归还对象
public synchronized void checkIn(T t) {
pool.get(t).setFree();
}
class ObjectStatus {
//占用
public void setUsing() {
}
//释放
public void setFree() {
//注意:若T是有状态,则需要回归到初始化状态
}
//检查是否可用
public boolean validate() {
return false;
}
}
//创建池化对象
public abstract T create();
}

这是一个简单的对象池实现,在实际应用中还需要考虑池的最小值、最大值、池化对象状态(若有的话,需要重点考虑)、异常处理(如满池情况)等方面,特别是池化对象状态,若是有状态的业务对象则需要重点关注。

38.2.3 最佳实践

把对象池化的本意是期望一次性初始化所有对象,减少对象在初始化上的昂贵性能开销,从而提高系统整体性能。然而池化处理本身也要付出代价,因此,并非任何情况下都适合采用对象池化。

通常情况下,在重复生成对象的操作成为影响性能的关键因素时,才适合进行对象池化。但是若池化所能带来的性能提高并不显著或重要的话,建议放弃对象池化技术,以保持代码的简明,转而使用更好的硬件来提高性能为佳。

对象池技术在Java领域已经非常成熟,只要做过企业级开发的人员,基本都用过C3P0、 DBCP、Proxool等连接池,也配置过minPoolSize、maxPoolSize等参数,这是对象池模式的典型应用。在实际开发中若需要对象池,建议使用common-pool工具包来实现,简单、快捷、 高效。

[^1]: 原文是Avoid expensive acquisition and release of resources by recycling objects that are no longer in use。

38.3 雇工模式

38.3.1 雇工合作

我是一个富豪(当然只是想象中的),家里有很多佣人,家务活基本上不用我动手,我只要动动口就可以了,在这里每个人都有不同分工,我可以指挥厨师把厨房弄干净,这是他的地盘;我可以指挥园丁把花园收拾干净、漂亮,这是他应该做的;我还可以让裁缝把我的衣服收拾干净。注意看,我这里列举出的三个对象(厨师、园丁、裁缝)都具有相同的功能:清洁。从另一方面说,厨房、花园、衣服都具有被清洁的特性,我们从这一例子入手, 编写代码如代码清单38-32所示。

代码清单38-32 三个对象的被清洁特质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//可以被清洁的对象 
public interface Cleanable {
//被清洁
public void celaned();
}
//花园
class Garden implements Cleanable{
public void celaned(){
System.out.println(“花园被清洁干净”);
}
}
//厨房
class Kitchen implements Cleanable{
public void celaned(){
System.out.println(“厨房被清洁干净”);
}
}
//衣服
class Cloth implements Cleanable{
public void celaned(){
System.out.println(“衣服被清洁干净”);
}
}

三个对象(厨房、花园、衣服)的共同特征抽取出来,同时也需要把厨师、裁缝、园丁的共同特征也抽象出来。从我这个主人的角度看来,他们三者都是清洁者,只是输入的对象不同而已,如代码清单38-33所示。

代码清单38-33 抽象的清洁者

1
2
3
4
5
6
public class Cleaner {
//清洁
public void clean(Cleanable clean){
clean.celaned();
}
}

三个对象(厨房、花园、衣服)的共同特征抽取出来,同时也需要把厨师、裁缝、园丁的共同特征也抽象出来。从我这个主人的角度看来,他们三者都是清洁者,只是输入的对象不同而已,如代码清单38-33所示。

代码清单38-33 抽象的清洁者

1
2
3
4
5
6
public class Cleaner {
//清洁
public void clean(Cleanable clean){
clean.celaned();
}
}

非常简单,就这么一个清洁者就可以厨师、园丁、裁缝。我们再编写一个场景类,描述一下发生了什么事,如代码清单38-34所示。

代码清单38-34 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
//厨师清洁厨房
Cleaner cookie = new Cleaner();
cookie.clean(new Kitchen());
//园丁清洁花园
Cleaner gardener = new Cleaner();
gardener.clean(new Garden());
//裁缝清洁衣服
Cleaner tailer = new Cleaner();
tailer.clean(new Cloth());
}
}

场景写完了,运行一下,就可以看到厨师打扫了厨房,园丁清洁了花园,裁缝清洁了衣服。代码很简单,但是诸位有没有发觉这和我们通常的分析是不同的。通常的做法是:既然厨师、园丁、裁缝都具有清洁的功能,那就定义一个接口描述三者的清洁功能,然后再定义三个类,分别代表厨师、园丁、裁缝实现这个接口。这是一种常用的解决办法,可以解决该问题,但今天我们从另外一个侧面进行分析,引出一个新的模式:雇工模式。

38.3.2 雇工模式的意图

雇工模式也叫做仆人模式(Servant Design Pattern),其意图是:

雇工模式是行为模式的一种,它为一组类提供通用的功能,而不需要类实现这些功能, 它是命令模式的一种扩展[^1]。

看看我们的例子,厨师、裁缝、园丁是一组类,都具有清洁的能力,但是我们却没实现,而是采用一种更优雅的方式来实现,这就是雇工模式。雇工模式的类图如图38-7所示。

image-20211001235152512

图38-7 雇工模式通用类图
在类图中,IServiced是用于定义“一组类”所具有的功能,其示例代码如代码清单38-35所示。

代码清单38-35 通用功能

1
2
3
4
public interface IServiced {
//具有的特质或功能
public void serviced();
}

针对不同的服务对象具备不同的服务内容,也就是具体的功能实现IServiced接口即可, 示例代码如代码清单38-36所示。

代码清单38-36 定义具体功能

1
2
3
4
5
6
7
8
public class Serviced1 implements IServiced {
public void serviced(){
}
}
public class Serviced2 implements IServiced{
public void serviced(){
}
}

功能定义完毕后,我们需要由一个雇工来执行这些功能。简单地说,就是需要有一个执行者,可以把一组功能聚集起来,示例代码如代码清单38-37所示。

代码清单38-37 雇工类

1
2
3
4
5
6
public class Servant {
//服务内容
public void service(IServiced serviceFuture){
serviceFuture.serviced();
}
}

在整个雇工模式中,所有具有IServiced功能的类可以实现该接口,然后由雇工类Servant 进行集合,完成一组类不用实现通用功能而具有相应职能的目的。

38.3.3 最佳实践

在日常的开发过程中,我们可能已经接触过雇工模式,只是我们没有把它抽取出来,也没有汇编成册。或许大家已经看出这与命令模式非常相似,读者可以回顾第15章,会发现雇工模式是命令模式的一种简化,但它更符合我们实际的需要,更容易引入开发场景中。

[^1]: 原文是A behavioral pattern used to offer some functionality to a group of classes without defining that functionality in each of them。

38.4 黑板模式

38.4.1 黑板模式的意图

黑板模式(Blackboard Design Pattern)是观察者模式的一个扩展,知名度并不高,但是我们使用的范围却非常广。黑板模式的意图如下:

允许消息的读写同时进行,广泛地交互消息[^1]。

简单地说,黑板模式允许多个消息读写者同时存在,消息的生产者和消费者完全分开。 这就像一个黑板,任何一个教授(消息的生产者)都可以在其上书写消息,任何一个学生 (消息的消费者)都可以从黑板上读取消息,两者在空间和时间上可以解耦,并且互不干扰。示意图如图38-8所示。

image-20211001235608017

图38-8 黑板模式示意图

看到这个图大家可能会说:这不是一个简单的消息广播吗?是的,确实如此,黑板模式确实是消息的广播,主要解决的问题是消息的生产者和消费者之间的耦合问题,它的核心是消息存储(黑板),它存储所有消息,并可以随时被读取。当消息生产者把消息写入到消息仓库后,其他消费者就可以从仓库中读取。当然,此时消息的写入者也可以变身为消息的阅读者,读写者在时间上解耦。对于这些消息,消费者只需要关注特定消息,不处理与自己不相关的消息,这一点通常通过过滤器来实现。

38.4.2 黑板模式的实现方法

黑板模式一般不会对架构产生什么影响,但它通常会要求有一个清晰的消息结构。黑板模式一般都会提供一系列的过滤器,以便消息的消费者不再接触到与自己无关的消息。在实际开发中,黑板模式常见的有两种实现方式。

  • 数据库作为黑板

利用数据库充当黑板,生产者更新数据信息,不同的消费者共享数据库中信息,这是最常见的实现方式。该方式在技术上容易实现,开发量较少,熟悉度较高。缺点是在大量消息和高频率访问的情况下,性能会受到一定影响。

在该模式下,消息的读取是通过消费者主动“拉取”,因此该模式也叫做“拉模式”。

  • 消息队列作为黑板

以消息队列作为黑板,通过订阅-发布模型即可实现黑板模式。这也是黑板模式被淡忘的一个重要原因:消息队列(Message Queue)已经非常普及了,做Java开发的已经没有几个不知道消息队列的。

在该模式下,消费者接收到的消息是被主动推送过来的,因此该模式也称为“推模式”。


提示 黑板模式不做详细讲解,因为我们现在已经在大量使用消息队列,既可以做到消息的同步处理,也可以实现异步处理,相信大家已经在开发中广泛使用了,它已经成为跨系统交互的一个事实标准了。


[^1]: 原文是allows multiple readers and writers. Communicates information system-wide。

38.5 空对象模式

空对象模式(Null Object Pattern)是通过实现一个默认的无意义对象来避免null值出现, 简单地说,就是为了避免在程序中出现null值判断而诞生的一种常用设计方法。

38.5.1 空对象模式的例子

举个简单的例子来说明,我们写一个听动物叫声的模拟程序,如代码清单38-38所示。

代码清单38-38 动物叫声

1
2
3
4
5
6
7
8
9
10
//定义动物接口
public interface Animal {
public void makeSound();
}
//定义一个小狗
class Dog implements Animal{
public void makeSound(){
System.out.println(“Wang Wang Wang!”);
}
}

然后再定义一个人来听动物的叫声,如代码清单38-39所示。

代码清单38-39 听动物叫声的人

1
2
3
4
5
6
7
8
public class Person {
//听到动物叫声
public void hear(Animal animal){
if(animal !=null){
animal.makeSound();
}
}
}

注意看粗体部分,也许你觉得程序没有什么问题,输入参数animal是应该做空值判断。 但是,我们这样思考:在一个完整的系统中,animal对象是如何产生?什么原因会产生null 值?如果我们能够控制住null值的产生,是不是就可以去掉这个空值判断了?那这样,程序是不是更易读更简单?好,我们就编写一个更完美的程序,增加一个NullAnimal类,如代码清单38-40所示。

代码清单38-40 增加一个NullAnimal

1
2
3
4
class NullAnimal implements Animal{
public void makeSound(){
}
}

增加了NullAnimal类后,在Person类中就不需要”animal!=null”这句话了,因为我们提供了一个实现接口的所有方法,不会再产生null对象。想象一个Web项目中,animal对象可能由MVC框架映射产生,我们只要定义一个默认的映射对象是NullAnimal,就可以解决空值判断的问题,提升代码的可读性。这就是空对象模式(一些项目组把它作为编码规范的一部分),非常简单,但非常实用。

38.5.2 最佳实践

空对象模式是通过空代码实现一个接口或抽象类的所有方法,以满足开发需求,简化程序。它如此简单,以至于我们经常在代码中看到和使用,对它已经熟视无睹了,而它无论是事前规划或事后重构,都不会对我们的代码产生太大冲击,这也是我们“藐视”它的根本原因。

14.1 进销存管理是这个样子的吗

大家都来自五湖四海,都要生存,于是都找了个靠山——公司,就是给你发薪水的地方。公司要想尽办法赢利赚钱,赢利方法则不尽相同,但是各个公司都有相同的三个环节: 采购、销售和库存。这个怎么说呢?比如一个软件公司,要开发软件,就需要购买开发环境,如Windows操作系统、数据库产品等,这就是采购;开发完产品还要把产品推销出去; 有产品就必然有库存,软件产品也有库存,虽然不需要占用库房空间,但也要占用光盘或硬盘,这也是库存。再比如做咨询服务的公司,它要采购什么?采购知识,采购经验,这是这类企业的生存之本,销售的也是知识和经验,库存同样是知识和经验。既然进销存是如此重要,我们今天就来讲讲它的原理和设计,我相信很多人都已经开发过这种类型的软件,基本上都形成了固定套路,不管是单机版还是网络版,一般的做法都是通过数据库来完成相关产品的管理,相对来说这还是比较简单的项目,三个模块的示意图如图14-1所示。

image-20210928193515742

图14-1 进销存示意图

我们从这个示意图上可以看出,三个模块是相互依赖的。我们就以一个终端销售商(以服务最终客户为目标的企业,比如某某超市、某某商店等)为例,采购部门要采购IBM的电脑,它根据以下两个要素来决定采购数量。

  • 销售情况

销售部门要反馈销售情况,畅销就多采购,滞销就不采购。

  • 库存情况

即使是畅销产品,库存都有1000台了,每天才卖出去10台,也就不需要再采购了!

销售模块是企业的赢利核心,对其他两个模块也有影响:

  • 库存情况

库房有货,才能销售,空手套白狼是不行的。

  • 督促采购

在特殊情况下,比如一个企业客户要一次性购买100台电脑,库存只有80台,这时需要催促采购部门赶快采购!

同样地,库存管理也对其他两个模块有影响。库房是有容积限制的,不可能无限大,所以就有了清仓处理,那就要求采购部门停止采购,同时销售部门进行打折销售。

从以上分析来看,这三个模块都有自己的行为,并且与其他模块之间的行为产生关联, 类似于我们办公室的同事,大家各干各的活,但是彼此之间还是有交叉的,于是彼此之间就产生紧耦合,也就是一个团队。我们先来实现这个进销存,类图如图14-2所示。

image-20210928193834540

图14-2 简单的进销存类图

Purchase负责采购管理,buyIBMComputer指定了采购IBM电脑,refuseBuyIBM是指不再采购IBM了,源代码如代码清单14-1所示。

代码清单14-1 采购管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Purchase {
//采购IBM电脑
public void buyIBMcomputer(int number){
//访问库存
Stock stock = new Stock();
//访问销售
Sale sale = new Sale();
//电脑的销售情况
int saleStatus = sale.getSaleStatus();
if(saleStatus>80){
//销售情况良好
System.out.println("采购IBM电脑:"+number + "台");
stock.increase(number);
}
else{
//销售情况不好
int buyNumber = number/2;
//折半采购
System.out.println("采购IBM电脑:"+buyNumber+ "台");
}
}
//不再采购IBM电脑
public void refuseBuyIBM(){
System.out.println("不再采购IBM电脑");
}
}

Purchase定义了采购电脑的标准:如果销售情况比较好,大于80分,你让我采购多少我就采购多少;销售情况不好,你让我采购100台,我就采购50台,对折采购。电脑采购完毕,需要放到库房中,因此要调用库存的方法,增加库存电脑数量。我们继续来看库房Stock类,如代码清单14-2所示。

代码清单14-2 库存管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Stock {
//刚开始有100台电脑
private static int COMPUTER_NUMBER =100;
//库存增加
public void increase(int number){
COMPUTER_NUMBER = COMPUTER_NUMBER + number;
System.out.println("库存数量为:"+COMPUTER_NUMBER);
}
//库存降低
public void decrease(int number){
COMPUTER_NUMBER = COMPUTER_NUMBER - number;
System.out.println("库存数量为:"+COMPUTER_NUMBER);
}
//获得库存数量
public int getStockNumber(){
return COMPUTER_NUMBER;
}
//存货压力大了,就要通知采购人员不要采购,销售人员要尽快销售
public void clearStock(){
Purchase purchase = new Purchase();
Sale sale = new Sale();
System.out.println("清理存货数量为:"+COMPUTER_NUMBER);
//要求折价销售
sale.offSale();
//要求采购人员不要采购
purchase.refuseBuyIBM();
}
}

库房中的货物数量肯定有增减,同时库房还有一个容量显示,达到一定的容量后就要求对一些商品进行折价处理,以腾出更多的空间容纳新产品。于是就有了clearStock方法,既然是清仓处理肯定就要折价销售了。于是在Sale类中就有了offSale方法,我们来看Sale源代码,如代码清单14-3所示。

代码清单14-3 销售管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Sale {
//销售IBM电脑
public void sellIBMComputer(int number){
//访问库存
Stock stock = new Stock();
//访问采购
Purchase purchase = new Purchase();
if(stock.getStockNumber()<number){
//库存数量不够销售
purchase.buyIBMcomputer(number);
}
System.out.println("销售IBM电脑"+number+"台");
stock.decrease(number);
}
//反馈销售情况,0~100之间变化,0代表根本就没人卖,100代表非常畅销,出一个卖一个
public int getSaleStatus(){
Random rand = new Random(System.currentTimeMillis());
int saleStatus = rand.nextInt(100);
System.out.println("IBM电脑的销售情况为:"+saleStatus);
return saleStatus;
}
//折价处理
public void offSale(){
//库房有多少卖多少
Stock stock = new Stock();
System.out.println("折价销售IBM电脑"+stock.getStockNumber()+"台");
}
}

Sale类中的getSaleStatus是获得销售情况,这个当然要出现在Sale类中了。记住要把恰当的类放到恰当的类中,销售情况只有销售人员才能反馈出来,通过百分制的机制衡量销售情况。我们再来看场景类是怎么运行的,场景类如代码清单14-4所示。

代码清单14-4 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
public static void main(String[] args) {
//采购人员采购电脑
System.out.println("------采购人员采购电脑--------");
Purchase purchase = new Purchase();
purchase.buyIBMcomputer(100);
//销售人员销售电脑
System.out.println("\n------销售人员销售电脑--------");
Sale sale = new Sale();
sale.sellIBMComputer(1);
//库房管理人员管理库存
System.out.println("\n------库房管理人员清库处理--------");
Stock stock = new Stock();
stock.clearStock();
}
}

我们在场景类中模拟了三种人员的活动:采购人员采购电脑,销售人员销售电脑,库管员管理库存。运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
------采购人员采购电脑-------- 
IBM电脑的销售情况为:95
采购IBM电脑:100台
库存数量为:200
------销售人员销售电脑--------
销售IBM电脑1台
库存数量为:199
------库房管理人员清库处理--------
清理存货数量为:199
折价销售IBM电脑199台
不再采购IBM电脑

运行结果也是我们期望的,三个不同类型的参与者完成了各自的活动。你有没有发现这三个类是彼此关联的?每个类都与其他两个类产生了关联关系。迪米特法则认为“每个类只和朋友类交流”,这个朋友类并非越多越好,朋友类越多,耦合性越大,要想修改一个就得修改一片,这不是面向对象设计所期望的,况且这还是仅三个模块的情况,属于比较简单的一个小项目。我们把进销存扩展一下,如图14-3所示。

image-20210928194905819

图14-3 扩展后的进销存示意图

这是一个蜘蛛网的结构,别说是编写程序了,就是给人看估计也能让一大批人昏倒!每个对象都需要和其他几个对象交流,对象越多,每个对象要交流的成本也就越大了,只是维护这些对象的交流就能让一大批程序员望而却步!从这方面来说,我们已经发现设计的缺陷了,作为一个架构师,发现缺陷就要想办法修改。

大家都学过网络的基本知识,网络拓扑有三种类型:总线型、环型、星型。星型网络拓扑如图14-4所示。

在星型网络拓扑中,每个计算机通过交换机和其他计算机进行数据交换,各个计算机之间并没有直接出现交互的情况。这种结构简单,而且稳定,只要中间那个交换机不瘫痪,整个网络就不会发生大的故障。公司和网吧一般都采用星型网络。我们是不是可以把这种星型结构引入到我们的设计中呢?我们先画一个示意图,如图14-5所示。

image-20210928195030758

图14-4 星型网络拓扑

image-20210928195111870

图14-5 修改后的进销存示意图

加入了一个中介者作为三个模块的交流核心,每个模块之间不再相互交流,要交流就通过中介者进行。每个模块只负责自己的业务逻辑,不属于自己的则丢给中介者来处理,简化了各模块之间的耦合关系,类图如图14-6所示。

image-20210928195223281

图14-6 修改后的进销存类图

建立了两个抽象类AbstractMediator和AbstractColeague,每个对象只是与中介者Mediator 之间产生依赖,与其他对象之间没有直接关系,AbstractMediator的作用是实现中介者的抽象定义,定义了一个抽象方法execute,如代码清单14-5所示。

代码清单14-5 抽象中介者

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class AbstractMediator {
protected Purchase purchase;
protected Sale sale;
protected Stock stock;
//构造函数
public AbstractMediator(){
purchase = new Purchase(this);
sale = new Sale(this);
stock = new Stock(this);
}
//中介者最重要的方法叫做事件方法,处理多个对象之间的关系
public abstract void execute(String str,Object...objects);
}

再来看具体的中介者,我们可以根据业务的要求产生多个中介者,并划分各中介者的职责。具体中介者如代码清单14-6所示。

代码清单14-6 具体中介者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class Mediator extends AbstractMediator {
//中介者最重要的方法
public void execute(String str,Object...objects){
if(str.equals("purchase.buy")){
//采购电脑
this.buyComputer((Integer)objects[0]);
}
else if(str.equals("sale.sell")){
//销售电脑
this.sellComputer((Integer)objects[0]);
}
else if(str.equals("sale.offsell")){
//折价销售
this.offSell();
}
else if(str.equals("stock.clear")){
//清仓处理
this.clearStock();
}
}
//采购电脑
private void buyComputer(int number){
int saleStatus = super.sale.getSaleStatus();
if(saleStatus>80){
//销售情况良好
System.out.println("采购IBM电脑:"+number + "台");
super.stock.increase(number);
}
else{
//销售情况不好
int buyNumber = number/2;
//折半采购
System.out.println("采购IBM电脑:"+buyNumber+ "台");
}
}
//销售电脑
private void sellComputer(int number){
if(super.stock.getStockNumber()<number){
//库存数量不够销售
super.purchase.buyIBMcomputer(number);
}
super.stock.decrease(number);
}
//折价销售电脑
private void offSell(){
System.out.println("折价销售IBM电脑"+stock.getStockNumber()+"台");
}
//清仓处理
private void clearStock(){
//要求清仓销售
super.sale.offSale();
//要求采购人员不要采购
super.purchase.refuseBuyIBM();
}
}

中介者Mediator定义了多个private方法,其目的是处理各个对象之间的依赖关系,就是说把原有一个对象要依赖多个对象的情况移到中介者的private方法中实现。在实际项目中, 一般的做法是中介者按照职责进行划分,每个中介者处理一个或多个类似的关联请求。

由于要使用中介者,我们增加了一个抽象同事类,三个具体的实现类分别继承该抽象类,如代码清单14-7所示。

代码清单14-7 抽象同事类

1
2
3
4
5
6
public abstract class AbstractColleague {
protected AbstractMediator mediator;
public AbstractColleague(AbstractMediator _mediator){
this.mediator = _mediator;
}
}

采购Purchase类如代码清单14-8所示。

代码清单14-8 修改后的采购管理

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Purchase extends AbstractColleague{
public Purchase(AbstractMediator _mediator){
super(_mediator);
}
//采购IBM电脑
public void buyIBMcomputer(int number){
super.mediator.execute("purchase.buy", number);
}
//不再采购IBM电脑
public void refuseBuyIBM(){
System.out.println("不再采购IBM电脑");
}
}

上述Purchase类简化了很多,也清晰了很多,处理自己的职责,与外界有关系的事件处理则交给了中介者来完成。再来看Stock类,如代码清单14-9所示。

代码清单14-9 修改后的库存管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Stock extends AbstractColleague {
public Stock(AbstractMediator _mediator){
super(_mediator);
}

//刚开始有100台电脑
private static int COMPUTER_NUMBER =100;
//库存增加
public void increase(int number){
COMPUTER_NUMBER = COMPUTER_NUMBER + number;
System.out.println("库存数量为:"+COMPUTER_NUMBER);
}
//库存降低
public void decrease(int number){
COMPUTER_NUMBER = COMPUTER_NUMBER - number;
System.out.println("库存数量为:"+COMPUTER_NUMBER);
}
//获得库存数量
public int getStockNumber(){
return COMPUTER_NUMBER;
}
//存货压力大了,就要通知采购人员不要采购,销售人员要尽快销售
public void clearStock(){
System.out.println("清理存货数量为:"+COMPUTER_NUMBER);
super.mediator.execute("stock.clear");
}
}

销售管理Sale类如代码清单14-10所示。

代码清单14-10 修改后的销售管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Sale extends AbstractColleague {
public Sale(AbstractMediator _mediator){
super(_mediator);
}
//销售IBM电脑
public void sellIBMComputer(int number){
super.mediator.execute("sale.sell", number);
System.out.println("销售IBM电脑"+number+"台");
}
//反馈销售情况,0~100变化,0代表根本就没人买,100代表非常畅销,出一个卖一个
public int getSaleStatus(){
Random rand = new Random(System.currentTimeMillis());
int saleStatus = rand.nextInt(100);
System.out.println("IBM电脑的销售情况为:"+saleStatus);
return saleStatus;
}
//折价处理
public void offSale(){
super.mediator.execute("sale.offsell");
}
}

增加了中介者,场景类也需要小小的改动,如代码清单14-11所示。

代码清单14-11 修改后的场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Client {
public static void main(String[] args) {
AbstractMediator mediator = new Mediator();
//采购人员采购电脑
System.out.println("------采购人员采购电脑--------");
Purchase purchase = new Purchase(mediator);
purchase.buyIBMcomputer(100);
//销售人员销售电脑
System.out.println("\n------销售人员销售电脑--------");
Sale sale = new Sale(mediator);
sale.sellIBMComputer(1);
//库房管理人员管理库存
System.out.println("\n------库房管理人员清库处理--------");
Stock stock = new Stock(mediator);
stock.clearStock();
}
}

在场景类中增加了一个中介者,然后分别传递到三个同事类中,三个类都具有相同的特性:只负责处理自己的活动(行为),与自己无关的活动就丢给中介者处理,程序运行的结果是相同的。从项目设计上来看,加入了中介者,设计结构清晰了很多,而且类间的耦合性大大减少,代码质量也有了很大的提升。

在多个对象依赖的情况下,通过加入中介者角色,取消了多个对象的关联或依赖关系, 减少了对象的耦合性。

16.1 古代妇女的枷锁——“三从四德”

中国古代对妇女制定了“三从四德”的道德规范,“三从”是指“未嫁从父、既嫁从夫、夫死从子”。也就是说,一位女性在结婚之前要听从于父亲,结婚之后要听从于丈夫,如果丈夫死了还要听从于儿子。举例来说,如果一位女性要出去逛街,在她出嫁前必须征得父亲的同意,出嫁之后必须获得丈夫的许可,那丈夫死了怎么办?那就得问问儿子是否允许自己出去逛街。估计你接下来马上要问:“要是没有儿子怎么办?”那就请示小叔子、侄子等。在父系社会中,妇女只占从属地位,现在想想中国古代的妇女还是挺悲惨的,连逛街都要多番请示。作为父亲、丈夫或儿子,只有两种选择:要不承担起责任来,允许她或不允许她逛街; 要不就让她请示下一个人,这是整个社会体系的约束,应用到我们项目中就是业务规则。下面来看如何通过程序来实现“三从”,需求很简单:通过程序描述一下古代妇女的“三从”制度。好,我们先来看类图,如图16-1所示。

image-20210929111938715

图16-1 妇女“三从”类图

类图非常简单,IHandler是三个有决策权对象的接口,IWomen是女性的代码,其实现也非常简单,IWomen如代码清单16-1所示。

代码清单16-1 女性接口

1
2
3
4
5
6
public interface IWomen {
//获得个人状况
public int getType();
//获得个人请示,你要干什么?出去逛街?约会?还是看电影?
public String getRequest();
}

女性接口仅两个方法,一个是取得当前的个人状况getType,通过返回值决定是结婚了还是没结婚、丈夫是否在世等,另外一个方法getRequest是要请示的内容,要出去逛街还是吃饭,其实现类如代码清单16-2所示。

代码清单16-2 古代妇女

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Women implements IWomen{
/** 通过一个int类型的参数来描述妇女的个人状况
* 1--未出嫁
* 2--出嫁
* 3--夫死
*/
private int type=0;
//妇女的请示
private String request = "";
//构造函数传递过来请求
public Women(int _type,String _request){
this.type = _type;
this.request = _request;
}
//获得自己的状况
public int getType(){
return this.type;
}
//获得妇女的请求
public String getRequest(){
return this.request;
}
}

我们使用数字来代表女性的不同状态:1是未结婚;2是已经结婚的,而且丈夫健在;3 是丈夫去世了。从整个设计上分析,有处理权的人(如父亲、丈夫、儿子)才是设计的核心,他们是要处理这些请求的,我们来看有处理权的人员接口IHandler,如代码清单16-3所示。

代码清单16-3 有处理权的人员接口

1
2
3
4
public interface IHandler {
//一个女性(女儿、妻子或者母亲)要求逛街,你要处理这个请求
public void HandleMessage(IWomen women);
}

非常简单,有处理权的人对妇女的请求进行处理,分别有三个实现类,在女儿没有出嫁之前父亲是有决定权的,其实现类如代码清单16-4所示。

代码清单16-4 父亲类

1
2
3
4
5
6
7
public class Father implements IHandler {
//未出嫁的女儿来请示父亲
public void HandleMessage(IWomen women) {
System.out.println("女儿的请示是:"+women.getRequest());
System.out.println("父亲的答复是:同意");
}
}

在女性出嫁后,丈夫有决定权,如代码清单16-5所示。

代码清单16-5 丈夫类

1
2
3
4
5
6
7
public class Husband implements IHandler {
//妻子向丈夫请示
public void HandleMessage(IWomen women) {
System.out.println("妻子的请示是:"+women.getRequest());
System.out.println("丈夫的答复是:同意");
}
}

在女性丧偶后,对母亲提出的请求儿子有决定权,如代码清单16-6所示。

代码清单16-6 儿子类

1
2
3
4
5
6
7
public class Son implements IHandler {
//母亲向儿子请示
public void HandleMessage(IWomen women) {
System.out.println("母亲的请示是:"+women.getRequest());
System.out.println("儿子的答复是:同意");
}
}

以上三个实现类非常简单,只有一个方法,处理女儿、妻子、母亲提出的请求,我们来模拟一下一个古代妇女出去逛街是如何请示的,如代码清单16-7所示。

代码清单16-7 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Client {
public static void main(String[] args) {
//随机挑选几个女性
Random rand = new Random();
ArrayList<IWomen> arrayList = new ArrayList();
for(int i=0;i<5;i++){
arrayList.add(new Women(rand.nextInt(4),"我要出去逛街"));
}
//定义三个请示对象
IHandler father = new Father();
IHandler husband = new Husband();
IHandler son = new Son();
for(IWomen women:arrayList){
if(women.getType() ==1){
//未结婚少女,请示父亲
System.out.println("\n--------女儿向父亲请示-------");
father.HandleMessage(women);
}
else if(women.getType() ==2){
//已婚少妇,请示丈夫
System.out.println("\n--------妻子向丈夫请示-------");
husband.HandleMessage(women);
}
else if(women.getType() == 3){
//母亲请示儿子
System.out.println("\n--------母亲向儿子请示-------");
son.HandleMessage(women);
}
else{
//暂时什么也不做
}
}
}
}

首先是通过随机方法产生了5个古代妇女的对象,然后看她们是如何就逛街这件事去请示的,运行结果如下所示(由于是随机的,您看到的结果可能和这里有所不同):

1
2
3
4
5
6
7
8
9
10
11
12
--------女儿向父亲请示------- 
女儿的请示是:我要出去逛街
父亲的答复是:同意
--------母亲向儿子请示-------
母亲的请示是:我要出去逛街
儿子的答复是:同意
--------妻子向丈夫请示-------
妻子的请示是:我要出去逛街
丈夫的答复是:同意
--------女儿向父亲请示-------
女儿的请示是:我要出去逛街
父亲的答复是:同意

“三从四德”的旧社会规范已经完整地表现出来了,你看谁向谁请示都定义出来了,但是你是不是发现这个程序写得有点不舒服?有点别扭?有点想重构它的感觉?那就对了!这段代码有以下几个问题:

  • 职责界定不清晰

对女儿提出的请示,应该在父亲类中做出决定,父亲有责任、有义务处理女儿的请示, 因此Father类应该是知道女儿的请求自己处理,而不是在Client类中进行组装出来,也就是说原本应该是父亲这个类做的事情抛给了其他类进行处理,不应该是这样的。

  • 代码臃肿

我们在Client类中写了if…else的判断条件,而且能随着能处理该类型的请示人员越多,if…else的判断就越多,想想看,臃肿的条件判断还怎么有可读性?!

  • 耦合过重

这是什么意思呢,我们要根据Women的type来决定使用IHandler的那个实现类来处理请求。有一个问题是:如果IHandler的实现类继续扩展怎么办?修改Client类?与开闭原则违背了!

  • 异常情况欠考虑

妻子只能向丈夫请示吗?如果妻子(比如一个现代女性穿越到古代了,不懂什么“三从四德”)向自己的父亲请示了,父亲应该做何处理?我们的程序上可没有体现出来,逻辑失败了!

既然有这么多的问题,那我们要想办法来解决这些问题,我们先来分析一下需求,女性提出一个请示,必然要获得一个答复,甭管是同意还是不同意,总之是要一个答复的,而且这个答复是唯一的,不能说是父亲作出一个决断,而丈夫也作出了一个决断,也即是请示传递出去,必然有一个唯一的处理人给出唯一的答复,OK,分析完毕,收工,重新设计,我们可以抽象成这样一个结构,女性的请求先发送到父亲类,父亲类一看是自己要处理的,就作出回应处理,如果女儿已经出嫁了,那就要把这个请求转发到女婿来处理,那女婿一旦去天国报道了,那就由儿子来处理这个请求,类似于如图16-2所示的顺序处理图。

image-20210929112713972

图16-2 女性请示的顺序处理图

父亲、丈夫、儿子每个节点有两个选择:要么承担责任,做出回应;要么把请求转发到后序环节。结构分析得已经很清楚了,那我们看怎么来实现这个功能,类图重新修正,如图16-3所示。

image-20210929112803611

图16-3 顺序处理的类图
从类图上看,三个实现类Father、Husband、Son只要实现构造函数和父类中的抽象方法response就可以了,具体由谁处理女性提出的请求,都已经转移到了Handler抽象类中,我们来看Handler怎么实现,如代码清单16-8所示。

代码清单16-8 修改后的Handler类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public abstract class Handler {
public final static int FATHER_LEVEL_REQUEST = 1;
public final static int HUSBAND_LEVEL_REQUEST = 2;
public final static int SON_LEVEL_REQUEST = 3;
//能处理的级别
private int level =0;
//责任传递,下一个人责任人是谁
private Handler nextHandler;
//每个类都要说明一下自己能处理哪些请求
public Handler(int _level){
this.level = _level;
}
//一个女性(女儿、妻子或者是母亲)要求逛街,你要处理这个请求
public final void HandleMessage(IWomen women){
if(women.getType() == this.level){
this.response(women);
}
else{
if(this.nextHandler != null){
//有后续环节,才把请求往后递送
this.nextHandler.HandleMessage(women);
}
else{
//已经没有后续处理人了,不用处理了
System.out.println("---没地方请示了,按不同意处理---\n");
}
}
}
/** 如果不属于你处理的请求,你应该让她找下一个环节的人,如女儿出嫁了,
* 还向父亲请示是否可以逛街,那父亲就应该告诉女儿,应该找丈夫请示
*/
public void setNext(Handler _handler){
this.nextHandler = _handler;
}
//有请示那当然要回应
protected abstract void response(IWomen women);
}

方法比较长,但是还是比较简单的,读者有没有看到,其实在这里也用到模板方法模式,在模板方法中判断请求的级别和当前能够处理的级别,如果相同则调用基本方法,做出反馈;如果不相等,则传递到下一个环节,由下一环节做出回应,如果已经达到环节结尾, 则直接做不同意处理。基本方法response需要各个实现类实现,每个实现类只要实现两个职责:一是定义自己能够处理的等级级别;二是对请求做出回应,我们首先来看首节点Father 类,如代码清单16-9所示。

代码清单16-9 父亲类

1
2
3
4
5
6
7
8
9
10
11
12
public class Father extends Handler {
//父亲只处理女儿的请求
public Father(){
super(Handler.FATHER_LEVEL_REQUEST);
}
//父亲的答复
protected void response(IWomen women) {
System.out.println("--------女儿向父亲请示-------");
System.out.println(women.getRequest());
System.out.println("父亲的答复是:同意\n");
}
}

丈夫类定义自己能处理的等级为2的请示,如代码清单16-10所示。

代码清单16-10 丈夫类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Husband extends Handler {
//丈夫只处理妻子的请求
public Husband(){

super(Handler.HUSBAND_LEVEL_REQUEST);
}
//丈夫请示的答复
protected void response(IWomen women) {
System.out.println("--------妻子向丈夫请示-------");
System.out.println(women.getRequest());
System.out.println("丈夫的答复是:同意\n");
}
}

儿子类只能处理等级为3的请示,如代码清单16-11所示。

代码清单16-11 儿子类

1
2
3
4
5
6
7
8
9
10
11
12
public class Son extends Handler {
//儿子只处理母亲的请求
public Son(){
super(Handler.SON_LEVEL_REQUEST);
}
//儿子的答复
protected void response(IWomen women) {
System.out.println("--------母亲向儿子请示-------");
System.out.println(women.getRequest());
System.out.println("儿子的答复是:同意\n");
}
}

这三个类都很简单,构造方法是必须实现的,父类框定子类必须有一个显式构造函数, 子类不实现编译不通过。通过构造方法我们设置了各个类能处理的请求类型,Father只能处理请求类型为1(也就是女儿)的请求;Husband只能处理请求类型类为2(也就是妻子)的请求,儿子只能处理请求类型为3(也就是母亲)的请求,那如果请求类型为4的该如何处理呢?在Handler中我们已经判断了,如何没有相应的处理者(也就是没有下一环节),则视为不同意。

Women类的接口没有任何变化,请参考图16-1所示。

实现类稍微有些变化,如代码清单16-12所示。

代码清单16-12 女性类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Women implements IWomen{
/** 通过一个int类型的参数来描述妇女的个人状况
* 1--未出嫁
* 2--出嫁
* 3--夫死
*/
private int type=0;
//妇女的请示
private String request = "";
//构造函数传递过来请求
public Women(int _type,String _request){
this.type = _type;
//为了便于显示,在这里做了点处理
switch(this.type){
case 1: this.request = "女儿的请求是:" + _request;
break;
case 2: this.request = "妻子的请求是:" + _request;
break;
case 3: this.request = "母亲的请求是:" + _request;
}
}
//获得自己的状况
public int getType(){
return this.type;
}
//获得妇女的请求
public String getRequest(){
return this.request;
}
}

为了展示结果清晰一点,Women类做了一些改变,如粗体部分所示。我们再来看Client 类是怎么描述古代这一个礼节的,如代码清单16-13所示。

代码清单16-13 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Client {
public static void main(String[] args) {
//随机挑选几个女性
Random rand = new Random();
ArrayList<IWomen> arrayList = new ArrayList();
for(int i=0;i<5;i++){
arrayList.add(new Women(rand.nextInt(4),"我要出去逛街"));
}
//定义三个请示对象
Handler father = new Father();
Handler husband = new Husband();
Handler son = new Son();
//设置请示顺序
father.setNext(husband);
husband.setNext(son);
for(IWomen women:arrayList){
father.HandleMessage(women);

}
}
}

在Client中设置请求的传递顺序,先向父亲请示,不是父亲应该解决的问题,则由父亲传递到丈夫类解决,若不是丈夫类解决的问题则传递到儿子类解决,最终的结果必然有一个返回,其运行结果如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--------妻子向丈夫请示------- 
妻子的请求是:我要出去逛街
丈夫的答复是:同意
--------女儿向父亲请示-------
女儿的请求是:我要出去逛街
父亲的答复是:同意
--------母亲向儿子请示-------
母亲的请求是:我要出去逛街
儿子的答复是:同意
--------妻子向丈夫请示-------
妻子的请求是:我要出去逛街
丈夫的答复是:同意
--------母亲向儿子请示-------
母亲的请求是:我要出去逛街
儿子的答复是:同意

结果也正确,业务调用类Client也不用去做判断到底是需要谁去处理,而且Handler抽象类的子类可以继续增加下去,只需要扩展传递链而已,调用类可以不用了解变化过程,甚至是谁在处理这个请求都不用知道。在这种模式下,即使现代社会的一个小太妹穿越到古代 (例如掉入时空隧道,或者时空突然扭转,甚至是突然魔法显灵),对“三从四德”没有任何了解也可以自由地应付,反正只要请示父亲就可以了,该父亲处理就父亲处理,不该父亲处理就往下传递。这就是责任链模式。

21.1 公司的人事架构是这样的吗

各位读者,大家在上学的时候应该都学过“数据结构”这门课程吧,还记得其中有一节叫“二叉树”吧,我们上学那会儿这一章节是必考内容,左子树,右子树,什么先序遍历、后序遍历,重点就是二叉树的遍历,我还记得当时老师就说,考试的时候一定有二叉树的构建和遍历,现在想起来还是觉得老师是正确的,树状结构在实际中应用非常广泛,想想看你最常使用的XML格式是不是就是一个树形结构。

咱就先说个最常见的例子,公司的人事管理就是一个典型的树状结构,想想看你公司的组织架构是不是如图21-1所示。

image-20210929170413274

图21-1 普遍的组织架构

从最高的老大,往下一层一层的管理,最后到我们这层小兵……很典型的树状结构(说明一下,这不是二叉树,有关二叉树的定义可以翻翻以前的教科书),我们今天的任务就是要把这个树状结构实现出来,并且还要把它遍历一遍,就类似于阅读你公司的人员花名册。

从该树状结构上分析,有两种不同性质的节点:有分支的节点(如研发部经理)和无分支的节点(如员工A、员工D等),我们增加一点学术术语上去,总经理叫做根节点(是不是想到XML中的那个根节点root,那就对了),类似研发部经理有分支的节点叫做树枝节点,类似员工A的无分支的节点叫做树叶节点,都很形象,三个类型的节点,那是不是定义三个类就可以?好,我们按照这个思路走下去,先看我们自己设计的类图,如图21-2所示。

image-20210929170508685

图21-2 最容易想到的组织架构类图

这个类图是初学者最容易想到的类图(首先声明,这个类图是有缺陷的,如果你已经看明白这个类图的缺陷了,该段落就可以一目十行地看下去,我们是循序渐进地讲课,一步一个脚印),非常简单,我们来看一下如何实现,先看最高级别的根节点接口,如代码清单21-1所示。

代码清单21-1 根节点接口

1
2
3
4
5
6
7
8
9
10
public interface IRoot {
//得到总经理的信息
public String getInfo();
//总经理下边要有小兵,那要能增加小兵,比如研发部总经理,这是个树枝节点
public void add(IBranch branch);
//那要能增加树叶节点
public void add(ILeaf leaf);
//既然能增加,那还要能够遍历,不可能总经理不知道他手下有哪些人
public ArrayList getSubordinateInfo();
}

这个根节点的对象就是我们的总经理,其具体实现如代码清单21-2所示。

代码清单21-2 根节点的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Root implements IRoot {
//保存根节点下的树枝节点和树叶节点,Subordinate的意思是下级
private ArrayList subordinateList = new ArrayList();
//根节点的名称
private String name = "";
//根节点的职位
private String position = "";
//根节点的薪水
private int salary = 0;
//通过构造函数传递进来总经理的信息
public Root(String name,String position,int salary){
this.name = name;
this.position = position;
this.salary = salary;
}
//增加树枝节点
public void add(IBranch branch) {
this.subordinateList.add(branch);
}
//增加叶子节点,比如秘书,直接隶属于总经理
public void add(ILeaf leaf) {
this.subordinateList.add(leaf);
}
//得到自己的信息
public String getInfo() {
String info = "";
info = "名称:"+ this.name;
;
info = info + "\t职位:" + this.position;
info = info + "\t薪水: " + this.salary;
return info;
}

//得到下级的信息
public ArrayList getSubordinateInfo() {
return this.subordinateList;
}
}

很简单,通过构造函数传入参数,然后获得信息,可以增加子树枝节点(部门经理)和叶子节点(秘书)。我们再来看其他有分支的节点接口,如代码清单21-3所示。

代码清单21-3 其他有分支的节点接口

1
2
3
4
5
6
7
8
9
10
public interface IBranch {
//获得信息
public String getInfo();
//增加数据节点,例如研发部下设的研发一组
public void add(IBranch branch);
//增加叶子节点
public void add(ILeaf leaf);
//获得下级信息
public ArrayList getSubordinateInfo();
}

有了接口,就应该有实现,其具体的实现类,如代码清单21-4所示。

代码清单21-4 分支的节点实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Branch implements IBranch {
//存储子节点的信息
private ArrayList subordinateList = new ArrayList();
//树枝节点的名称
private String name="";
//树枝节点的职位
private String position = "";
//树枝节点的薪水
private int salary = 0;
//通过构造函数传递树枝节点的参数
public Branch(String name,String position,int salary){
this.name = name;
this.position = position;
this.salary = salary;
}
//增加一个子树枝节点
public void add(IBranch branch) {
this.subordinateList.add(branch);
}
//增加一个叶子节点
public void add(ILeaf leaf) {
this.subordinateList.add(leaf);
}
//获得自己树枝节点的信息
public String getInfo() {

String info = "";
info = "名称:" + this.name;
info = info + "\t职位:"+ this.position;
info = info + "\t薪水:"+this.salary;
return info;
}
//获得下级的信息
public ArrayList getSubordinateInfo() {
return this.subordinateList;
}
}

不管是总经理还是部门经理都是有子节点的存在,最终的子节点就是叶子节点,其接口如代码清单21-5所示。

代码清单21-5 叶子节点的接口

1
2
3
4
public interface ILeaf {
//获得自己的信息
public String getInfo();
}

叶子节点的接口简单,实现也非常容易,如代码清单21-6所示。

代码清单21-6 叶子节点的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Leaf implements ILeaf {
//叶子叫什么名字
private String name = "";
//叶子的职位
private String position = "";
//叶子的薪水
private int salary=0;
//通过构造函数传递信息
public Leaf(String name,String position,int salary){
this.name = name;
this.position = position;
this.salary = salary;
}
//最小的小兵只能获得自己的信息了
public String getInfo() {
String info = "";
info = "名称:" + this.name;
info = info + "\t职位:"+ this.position;
info = info + "\t薪水:"+this.salary;
return info;
}
}

好了,所有的根节点、树枝节点和叶子节点都已经实现了,从总经理、部门经理到最终的员工都已经实现,然后的工作就是组装成一个树状结构并遍历这棵树,通过什么来完成呢?通过场景类Client完成,如代码清单21-7所示。

代码清单21-7 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class Client {
public static void main(String[] args) {
//首先产生了一个根节点
IRoot ceo = new Root("王大麻子","总经理",100000);
//产生三个部门经理,也就是树枝节点
IBranch developDep = new Branch("刘大瘸子","研发部门经理",10000);
IBranch salesDep = new Branch("马二拐子","销售部门经理",20000);
IBranch financeDep = new Branch("赵三驼子","财务部经理",30000);
//再把三个小组长产生出来
IBranch firstDevGroup = new Branch("杨三乜斜","开发一组组长",5000);
IBranch secondDevGroup = new Branch("吴大棒槌","开发二组组长",6000);
//剩下的就是我们这些小兵了,就是路人甲、路人乙
ILeaf a = new Leaf("a","开发人员",2000);
ILeaf b = new Leaf("b","开发人员",2000);
ILeaf c = new Leaf("c","开发人员",2000);
ILeaf d = new Leaf("d","开发人员",2000);
ILeaf e = new Leaf("e","开发人员",2000);
ILeaf f = new Leaf("f","开发人员",2000);
ILeaf g = new Leaf("g","开发人员",2000);
ILeaf h = new Leaf("h","销售人员",5000);
ILeaf i = new Leaf("i","销售人员",4000);
ILeaf j = new Leaf("j","财务人员",5000);
ILeaf k = new Leaf("k","CEO秘书",8000);
ILeaf zhengLaoLiu = new Leaf("郑老六","研发部副总",20000);
//该产生的人都产生出来了,然后我们怎么组装这棵树
//首先是定义总经理下有三个部门经理
ceo.add(developDep);
ceo.add(salesDep);
ceo.add(financeDep);
//总经理下还有一个秘书
ceo.add(k);
//定义研发部门下的结构
developDep.add(firstDevGroup);
developDep.add(secondDevGroup);
//研发部经理下还有一个副总
developDep.add(zhengLaoLiu);
//看看开发两个开发小组下有什么
firstDevGroup.add(a);
firstDevGroup.add(b);
firstDevGroup.add(c);
secondDevGroup.add(d);
secondDevGroup.add(e);
secondDevGroup.add(f);
//再看销售部下的人员情况
salesDep.add(h);
salesDep.add(i);
//最后一个财务
financeDep.add(j);
//打印写完的树状结构
System.out.println(ceo.getInfo());
//打印出来整个树形
getAllSubordinateInfo(ceo.getSubordinateInfo());
}
//遍历所有的树枝节点,打印出信息
private static void getAllSubordinateInfo(ArrayList subordinateList){
int length = subordinateList.size();
//定义一个ArrayList长度,不要在for循环中每次计算
for(int m=0;m<length;m++){
Object s = subordinateList.get(m);
if(s instanceof Leaf){
//是个叶子节点,也就是员工
ILeaf employee = (ILeaf)s;
System.out.println(((Leaf) s).getInfo());
}
else{
IBranch branch = (IBranch)s;
System.out.println(branch.getInfo());
//再递归调用
getAllSubordinateInfo(branch.getSubordinateInfo());
}
}
}
}

这个程序比较长,如果在我们的项目中有这样的程序,肯定是要被拉出来做典型的,你写一大坨的程序给谁呀,以后还要维护,程序要短小精悍!幸运的是,我们这是作为案例来讲解,而且就是指出这样组装这棵树是有问题的,等会我们深入讲解,先看运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
名称:王大麻子 职位:总经理 薪水: 100000 
名称:刘大瘸子 职位:研发部门经理 薪水:10000
名称:杨三乜斜 职位:开发一组组长 薪水:5000
名称:a 职位:开发人员 薪水:2000
名称:b 职位:开发人员 薪水:2000
名称:c 职位:开发人员 薪水:2000
名称:吴大棒槌 职位:开发二组组长 薪水:6000
名称:d 职位:开发人员 薪水:2000
名称:e 职位:开发人员 薪水:2000
名称:f 职位:开发人员 薪水:2000
名称:郑老六 职位:研发部副总 薪水:20000
名称:马二拐子 职位:销售部门经理 薪水:20000
名称:h 职位:销售人员 薪水:5000
名称:i 职位:销售人员 薪水:4000
名称:赵三驼子 职位:财务部经理 薪水:30000
名称:j 职位:财务人员 薪水:5000
名称:k 职位:CEO秘书 薪水:8000

和我们期望的结果一样,一棵完整的树就生成了,而且我们还能够遍历。不错,不错, 但是看类图或程序的时候,你有没有发觉有问题?getInfo每个接口都有,为什么不能抽象出来?Root类和Branch类有什么差别?根节点本身就是树枝节点的一种,为什么要定义成两个接口两个类?如果我要加一个任职期限,你是不是每个类都需要修改?如果我要后序遍历 (从员工找到他的上级领导)能做到吗?——彻底晕菜了!

问题很多,我们一个一个解决,先说抽象的问题。我们确实可以把IBranch和IRoot合并成一个接口,确认无疑的事我们先做,那我们就修改一下类图,如图21-3所示。

仔细看看这个类图,还能不能发现点问题。想想看接口的作用是什么?定义一类事物所具有的共性,那ILeaf和IBranch是不是也有共性呢?有,getInfo方法!我们是不是要把这个共性也封装起来呢?是的,是的,提炼事物的共同点,然后封装之,这是我们作为设计专家的拿手好戏,修改后的类图如图21-4所示。

image-20210929171106675

图21-3 整合根节点和树枝节点后的类图

image-20210929171143357

图21-4 修改后的类图

类图上增加了一个ICorp接口,它是公司所有人员信息的接口类,不管你是经理还是员工,你都有名字、职位、薪水,这个定义成一个接口没有错,但是你可能对于ILeaf接口持怀疑状态,空接口有何意义呀?有意义!它是每个树枝节点的代表,系统扩容的时候你就会发现它是多么“栋梁”。我们先来看新增加的接口ICorp,如代码清单21-8所示。

代码清单21-8 公司人员接口

1
2
3
4
public interface ICorp {
//每个员工都有信息,你想隐藏,门儿都没有!
public String getInfo();
}

接口很简单,只有一个方法,就是获得员工的信息,树叶节点是最基层的构件,我们先来看看它的接口,空接口,如代码清单21-9所示。

代码清单21-9 树叶接口

1
2
public interface ILeaf extends ICorp {
}

树叶接口的实现类,如代码清单21-10所示。

代码清单21-10 树叶接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Leaf implements ILeaf {
//小兵也有名称
private String name = "";
//小兵也有职位
private String position = "";
//小兵也有薪水,否则谁给你干
private int salary = 0;
//通过一个构造函数传递小兵的信息
public Leaf(String name,String position,int salary){
this.name = name;
this.position = position;
this.salary = salary;
}
//获得小兵的信息
public String getInfo() {
String info = "";
info = "姓名:" + this.name;
info = info + "\t职位:"+ this.position;
info = info + "\t薪水:" + this.salary;
return info;
}
}

小兵就只有这些信息了,我们是具体干活的,我们是管理不了其他同事的,我们来看看那些经理和小组长是怎么实现的,也就是IBranch接口,如代码清单21-11所示。

代码清单21-11 树枝接口

1
2
3
4
5
6
7
8
public interface IBranch extends ICorp {
//能够增加小兵(树叶节点)或者是经理(树枝节点)
public void addSubordinate(ICorp corp);
//我还要能够获得下属的信息
public ArrayList<ICorp> getSubordinate();
/*本来还应该有一个方法delSubordinate(ICorp corp),删除下属
* 这个方法我们没有用到就不写进来了 */
}

接口也很简单,其实现类也不可能太复杂,如代码清单21-12所示。

代码清单21-12 树枝实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Branch implements IBranch {
//领导也是人,也有名字
private String name = "";
//领导和领导不同,也是职位区别
private String position = "";
//领导也是拿薪水的
private int salary = 0;
//领导下边有哪些下级领导和小兵
ArrayList<ICorp> subordinateList = new ArrayList<ICorp>();
//通过构造函数传递领导的信息
public Branch(String name,String position,int salary){
this.name = name;
this.position = position;
this.salary = salary;
}
//增加一个下属,可能是小头目,也可能是个小兵
public void addSubordinate(ICorp corp) {
this.subordinateList.add(corp);
}
//我有哪些下属
public ArrayList<ICorp> getSubordinate() {
return this.subordinateList;
}
//领导也是人,他也有信息
public String getInfo() {
String info = "";
info = "姓名:" + this.name;
info = info + "\t职位:"+ this.position;
info = info + "\t薪水:" + this.salary;
return info;
}
}

实现类也很简单,不多说,程序写得好不好,就看别人怎么调用了,我们看场景类Client,如代码清单21-13所示。

代码清单21-13 场景类a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class Client {
public static void main(String[] args) {
//首先是组装一个组织结构出来
Branch ceo = compositeCorpTree();
//首先把CEO的信息打印出来
System.out.println(ceo.getInfo());
//然后是所有员工信息
System.out.println(getTreeInfo(ceo));
}
//把整个树组装出来
public static Branch compositeCorpTree(){
//首先产生总经理CEO
Branch root = new Branch("王大麻子","总经理",100000);
//把三个部门经理产生出来
Branch developDep = new Branch("刘大瘸子","研发部门经理",10000);
Branch salesDep = new Branch("马二拐子","销售部门经理",20000);
Branch financeDep = new Branch("赵三驼子","财务部经理",30000);
//再把三个小组长产生出来
Branch firstDevGroup = new Branch("杨三乜斜","开发一组组长",5000);
Branch secondDevGroup = new Branch("吴大棒槌","开发二组组长",6000);
//把所有的小兵都产生出来
Leaf a = new Leaf("a","开发人员",2000);
Leaf b = new Leaf("b","开发人员",2000);
Leaf c = new Leaf("c","开发人员",2000);
Leaf d = new Leaf("d","开发人员",2000);
Leaf e = new Leaf("e","开发人员",2000);
Leaf f = new Leaf("f","开发人员",2000);
Leaf g = new Leaf("g","开发人员",2000);
Leaf h = new Leaf("h","销售人员",5000);
Leaf i = new Leaf("i","销售人员",4000);
Leaf j = new Leaf("j","财务人员",5000);
Leaf k = new Leaf("k","CEO秘书",8000);
Leaf zhengLaoLiu = new Leaf("郑老六","研发部副经理",20000);
//开始组装
//CEO下有三个部门经理和一个秘书
root.addSubordinate(k);
root.addSubordinate(developDep);
root.addSubordinate(salesDep);
root.addSubordinate(financeDep);
//研发部经理
developDep.addSubordinate(zhengLaoLiu);
developDep.addSubordinate(firstDevGroup);
developDep.addSubordinate(secondDevGroup);
//看看两个开发小组下有什么
firstDevGroup.addSubordinate(a);
firstDevGroup.addSubordinate(b);
firstDevGroup.addSubordinate(c);
secondDevGroup.addSubordinate(d);
secondDevGroup.addSubordinate(e);
secondDevGroup.addSubordinate(f);
//再看销售部下的人员情况
salesDep.addSubordinate(h);
salesDep.addSubordinate(i);
//最后一个财务
financeDep.addSubordinate(j);
return root;
}
//遍历整棵树,只要给我根节点,我就能遍历出所有的节点
public static String getTreeInfo(Branch root){
ArrayList<ICorp> subordinateList = root.getSubordinate();
String info = "";
for(ICorp s :subordinateList){
if(s instanceof Leaf){
//是员工就直接获得信息
info = info + s.getInfo()+"\n";
}
else{
//是个小头目
info = info + s.getInfo() +"\n"+ getTreeInfo((Branch)s);
}
}
return info;
}

}

运行结果完全相同,不再赘述。通过这样构件,一个非常清晰的树状人员资源管理图出现了,那我们的程序是否还可以优化?可以!你看Leaf和Branch中都有getInfo信息,是不是可以抽象?好,我们抽象一下,如图21-5所示。

image-20210929171702993

图21-5 精简的类图
你一看这个图,乐了。能不乐嘛,减少很多工作量了,接口没有了,改成抽象类了,IBranch接口也没有了,直接把方法放到了实现类中了,太精简了!而且场景类只认定抽象类Corp就成,那我们首先来看抽象类ICorp,如代码清单21-14所示。

代码清单21-14 抽象公司职员类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class Corp {
//公司每个人都有名称
private String name = "";
//公司每个人都职位
private String position = "";
//公司每个人都有薪水
private int salary =0;
public Corp(String _name,String _position,int _salary){
this.name = _name;
this.position = _position;
this.salary = _salary;
}
//获得员工信息
public String getInfo(){
String info = "";
info = "姓名:" + this.name;

info = info + "\t职位:"+ this.position;
info = info + "\t薪水:" + this.salary;
return info;
}
}

抽象类嘛,就应该抽象出一些共性的东西出来,然后看两个具体的实现类,树叶节点如代码清单21-15所示。

代码清单21-15 树叶节点

1
2
3
4
5
6
public class Leaf extends Corp {
//就写一个构造函数,这个是必需的
public Leaf(String _name,String _position,int _salary){
super(_name,_position,_salary);
}
}

这个精简得比较多,几行代码就完成了,确实就应该这样,下面是小头目的实现类,如代码清单21-16所示。

代码清单21-16 树枝节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Branch extends Corp {
//领导下边有哪些下级领导和小兵
ArrayList<Corp> subordinateList = new ArrayList<Corp>();
//构造函数是必需的
public Branch(String _name,String _position,int _salary){
super(_name,_position,_salary);
}
//增加一个下属,可能是小头目,也可能是个小兵
public void addSubordinate(Corp corp) {
this.subordinateList.add(corp);
}
//我有哪些下属
public ArrayList<Corp> getSubordinate() {
return this.subordinateList;
}
}

场景类中构建树形结构,并进行遍历。组装没有变化,遍历组织机构数稍有变化,如代码清单21-17所示。

代码清单21-17 稍稍修改的场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Client {
//遍历整棵树,只要给我根节点,我就能遍历出所有的节点
public static String getTreeInfo(Branch root){
ArrayList<Corp> subordinateList = root.getSubordinate();
String info = "";
for(Corp s :subordinateList){
if(s instanceof Leaf){
//是员工就直接获得信息
info = info + s.getInfo()+"\n";
}
else{
//是个小头目
info = info+s.getInfo()+"\n"+ getTreeInfo((Branch)s);
}
}
return info;
}
}

场景类中main方法没有变动,请参考代码清单21-7所示,不再赘述。遍历组织机构树的getTreeInfo稍有修改,就是把用到ICorp接口的地方修改为Corp抽象类,仅仅修改了粗体部分,其他保持不变,运行结果相同。这就是组合模式。

22.1 韩非子身边的卧底是谁派来的

《孙子兵法》有云:“知彼知己,百战不殆;不知彼而知己,一胜一负;不知彼,不知己,每战必殆”,那怎么才能知己知彼呢?知己是很容易的,自己的军队嘛,很容易知根知底,那怎么知彼呢?安插间谍是个好办法,这是古今中外屡试不爽的方法,我们今天就来讲一个间谍的故事。

韩非子大家都应该记得吧,法家的代表人物,主张建立法制社会,实施重罚制度,真是非常有远见呀!看看现在社会在呼吁什么,建立法制化的社会,这在2000多年前就已经提出了。大家可能还不知道,法家还有一个非常重要的代表人物——李斯。李斯是秦国的丞相, 最终被残忍车裂的那位,李斯和韩非子都是荀子的学生,李斯是师兄,韩非子是师弟,若干年后,李斯成为最强诸侯国秦国的上尉,致力于统一全国,于是安插了间谍到各个国家的重要人物的身边,以获取必要的信息,韩非子作为韩国的重量级人物,身边自然有不少间谍, 韩非子做的事,李斯都了如指掌,那可是相隔千里!怎么做到的呢?间谍呀!我们先通过程序把这个过程展现一下,看看李斯是怎么监控韩非子的,先看两个主角的类图,如图22-1所示。

image-20210929193001857

图22-1 监控者和被监控者
仅有这两个对象还是不够的,我们要解决的是李斯是怎么监控韩非子的?创建一个后台线程一直处于运行状态,一旦发现韩非子在吃饭或者娱乐就触发事件?这是真实世界的翻版,安排了一个间谍,观察韩非子的生活起居,并上报给李斯,然后李斯再触发update事件,类图继续扩充,如图22-2所示。

image-20210929193132294

图22-2 通过后台线程监控

这个类图应该是程序员最容易想到的,你要监控,我就给你找个间谍角色(Spy类), 我们来看程序的实现,先看我们的主角韩非子的接口(类似于韩非子这样的人,被观察者角色),如代码清单22-1所示。

代码清单22-1 被观察者接口

1
2
3
4
5
6
public interface IHanFeiZi {
//韩非子也是人,也要吃早饭的
public void haveBreakfast();
//韩非之也是人,是人就要娱乐活动
public void haveFun();
}

对接口进行扩充,增加了两个状态isHavingBreakfast(是否在吃早饭)和isHavingFun(是否在娱乐),以方便Spy进行监控,如代码清单22-2所示。

代码清单22-2 具体的被观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class HanFeiZi implements IHanFeiZi{
//韩非子是否在吃饭,作为监控的判断标准
private boolean isHavingBreakfast = false;
//韩非子是否在娱乐
private boolean isHavingFun = false;
//韩非子要吃饭了
public void haveBreakfast(){
System.out.println("韩非子:开始吃饭了...");
this.isHavingBreakfast =true;
}
//韩非子开始娱乐了
public void haveFun(){
System.out.println("韩非子:开始娱乐了...");
this.isHavingFun = true;
}
//以下是bean的基本方法,getter/setter,不多说
public boolean isHavingBreakfast() {
return isHavingBreakfast;
}
public void setHavingBreakfast(boolean isHavingBreakfast) {
this.isHavingBreakfast = isHavingBreakfast;
}
public boolean isHavingFun() {
return isHavingFun;
}
public void setHavingFun(boolean isHavingFun) {
this.isHavingFun = isHavingFun;
}
}

其中有两个getter/setter方法,这个就没有在类图中表示出来,比较简单,通过isHavingBreakfast和isHavingFun这两个布尔型变量来判断韩非子是否在吃饭或者娱乐,韩非子属于被观察者,那还有观察者李斯,我们来看李斯的接口,如代码清单22-3所示。

代码清单22-3 抽象观察者

1
2
3
4
public interface ILiSi {
//一发现别人有动静,自己也要行动起来
public void update(String context);
}

李斯这类人比较简单,一发现自己观察的对象发生了变化,比如吃饭、娱乐,自己立刻也要行动起来,怎么行动呢?如代码清单22-4所示。

代码清单22-4 韩非子

1
2
3
4
5
6
7
8
9
10
11
12
public class LiSi implements ILiSi{
//首先李斯是个观察者,一旦韩非子有活动,他就知道,他就要向老板汇报
public void update(String str){
System.out.println("李斯:观察到韩非子活动,开始向老板汇报了...");
this.reportToQinShiHuang(str);
System.out.println("李斯:汇报完毕...\n");
}
//汇报给秦始皇
private void reportToQinShiHuang(String reportContext){
System.out.println("李斯:报告,秦老板!韩非子有活动了--->"+reportContext);
}
}

两个重量级的人物都定义出来了,间谍这个“卑鄙”小人是不是也要登台了,如代码清单22-5所示。

代码清单22-5 间谍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Spy extends Thread{
private HanFeiZi hanFeiZi;
private LiSi liSi;
private String type;
//通过构造函数传递参数,我要监控的是谁,谁来监控,要监控什么
public Spy(HanFeiZi _hanFeiZi,LiSi _liSi,String _type){
this.hanFeiZi =_hanFeiZi;
this.liSi = _liSi;
this.type = _type;
}
@Override
public void run(){
while(true){
if(this.type.equals("breakfast")){
//监控是否在吃早餐
//如果发现韩非子在吃饭,就通知李斯
if(this.hanFeiZi.isHavingBreakfast()){
this.liSi.update("韩非子在吃饭");
//重置状态,继续监控
this.hanFeiZi.setHavingBreakfast(false);
}
}
else{
//监控是否在娱乐
if(this.hanFeiZi.isHavingFun()){
this.liSi.update("韩非子在娱乐");
this.hanFeiZi.setHavingFun(false);
}
}
}
}
}

监控程序继承了java.lang.Thread类,可以同时启动多个线程进行监控,Java的多线程机制还是比较简单的,继承Thread类,重写run()方法,然后new SubThread(),再然后subThread.start()就可以启动一个线程了。我们建立一个场景类来回顾一下这段历史,如代码清单22-6所示。

代码清单22-6 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Client {
public static void main(String[] args) throws InterruptedException {
//定义出韩非子和李斯
LiSi liSi = new LiSi();
HanFeiZi hanFeiZi = new HanFeiZi();
//观察早餐
Watch watchBreakfast = new Watch(hanFeiZi,liSi,"breakfast");
//开始启动线程,监控
watchBreakfast.start();
//观察娱乐情况
Watch watchFun = new Watch(hanFeiZi,liSi,"fun");
watchFun.start();
//然后我们看看韩非子在干什么
Thread.sleep(1000);
//主线程等待1秒后后再往下执行
hanFeiZi.haveBreakfast();
//韩非子娱乐了
Thread.sleep(1000);
hanFeiZi.haveFun();
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
韩非子:开始吃饭了... 
李斯:观察到韩非子活动,开始向老板汇报了...
李斯:报告,秦老板!韩非子有活动了--->韩非子在吃饭
李斯:汇报完毕
韩非子:开始娱乐了...
李斯:观察到韩非子活动,开始向老板汇报了...
李斯:报告,秦老板!韩非子有活动了--->韩非子在娱乐
李斯:汇报完毕

结果出来,韩非子一吃早饭李斯就知道,韩非子一娱乐李斯也知道,非常正确!结果正确但并不表示你有成绩,我告诉你:你的成绩是0,甚至是负的!你有没有看到你的CPU飙升,Eclipse不响应状态?看到了?看到了你还不想为什么?!看看上面的程序,别的就不多说了,使用了一个死循环while(true)来做监听,要是用到项目中,你要多少硬件投入进来? 你还让不让别人的程序运行了?!一台服务器就跑你这一个程序就完事!

错误也看到了,我们必须要修改,这个没法应用到项目中,而且这个程序根本就不是面向对象的程序,这完全是面向过程的,不改不行,怎么修改呢?我们来想,既然韩非子一吃饭李斯就知道了,那我们为什么不把李斯这个类聚集到韩非子那个类上呢?说改就改,立马动手,我们来看修改后的类图,如图22-3所示。

类图非常简单,就是在HanFeiZi类中引用了LiSi实例,看我们程序代码怎么修改,IHanFeiZi接口完全没有修改,可以参考代码清单22-1所示。我们来看实现类的修改,如代码清单22-7所示。

代码清单22-7 通过聚集方式的被观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HanFeiZi implements IHanFeiZi{
//把李斯声明出来
private ILiSi liSi =new LiSi();
//韩非子要吃饭了
public void haveBreakfast(){
System.out.println("韩非子:开始吃饭了...");
//通知李斯
this.liSi.update("韩非子在吃饭");
}
//韩非子开始娱乐了
public void haveFun(){
System.out.println("韩非子:开始娱乐了...");
this.liSi.update("韩非子在娱乐");
}
}

image-20210929195135289

图22-3 通过聚集方式监控

韩非子HanFeiZi实现类就把接口的两个方法实现就可以了,在每个方法中调用LiSi.update()方法,完成李斯观察韩非子的职责,李斯的接口和实现类都没有任何改变,请参考代码清单22-3、22-4。我们再来看看Client程序的变更,如代码清单22-8所示。

代码清单22-8 通过聚集方式的场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//定义出韩非子
HanFeiZi hanFeiZi = new HanFeiZi();
//然后我们看看韩非子在干什么
hanFeiZi.haveBreakfast();
//韩非子娱乐了
hanFeiZi.haveFun();
}
}

李斯就不用在场景类中定义了,非常简单,运行结果相同,不再赘述。

我们思考一下,修改后的程序运行结果正确,效率也比较高,是不是应该乐呵乐呵了? 大功告成了?稍等等,你想在战国争雄的时候,韩非子这么有名望、有实力的人,就只有秦国关心他吗?想想也不可能呀,确实有一大帮的各国类似于李斯这样的人在看着他,监视着他的一举一动,但是看看我们的程序,你在HanFeiZi这个类中定义:

1
private ILiSi liSi =new LiSi();

这样一来只有李斯才能观察到韩非子,这是不对的,也就是说韩非子的活动只通知了李斯一个人,这不可能;再者说了,李斯只观察韩非子的吃饭、娱乐吗?政治倾向不关心吗? 思维倾向不关心吗?杀人放火不关心吗?也就说韩非子的一系列活动都要通知李斯,这可怎么办?要按照上面的例子,我们如何修改?这和开闭原则严重违背呀,我们的程序有问题, 修改如图22-4所示。

image-20210929195341469

图22-4 改进后的观察者和被观察者

我们把原有类图做了两个修改:

  • 增加Observable

实现该接口的都是被观察者,那韩非子是被观察者,他当然也要实现该接口了,同时他还有与其他庸人相异的事要做,因此他还是要实现IHanFeizi接口。

  • 修改ILiSI接口名称为Observer

接口名称修改了一下,这样显得更抽象化,所有实现该接口的都是观察者(类似李斯这样的)。

Observable是被观察者,就是类似韩非子这样的人,在Observable接口中有三个比较重要的方法,分别是addObserver增加观察者,deleteObserver删除观察者,notifyObservers通知所有的观察者,这是什么意思呢?我这里有一个信息,一个对象,我可以允许有多个对象来察看,你观察也成,我观察也成,只要是观察者就成,也就是说我的改变或动作执行,会通知其他的对象,看程序会更明白一点,先看Observable接口,如代码清单22-9所示。

代码清单22-9 被观察者接口

1
2
3
4
5
6
7
8
public interface Observable {
//增加一个观察者
public void addObserver(Observer observer);
//删除一个观察者
public void deleteObserver(Observer observer);
//既然要观察,我发生改变了他也应该有所动作,通知观察者
public void notifyObservers(String context);
}

这是一个通用的被观察者接口,所有的被观察者都可以实现这个接口。再来看韩非子的实现类,如代码清单22-10所示。

代码清单22-10 被观察者实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class HanFeiZi implements Observable ,IHanFeiZi{
//定义个变长数组,存放所有的观察者
private ArrayList<Observer> observerList = new ArrayList<Observer>();
//增加观察者
public void addObserver(Observer observer){
this.observerList.add(observer);
}
//删除观察者
public void deleteObserver(Observer observer){
this.observerList.remove(observer);
}
//通知所有的观察者
public void notifyObservers(String context){
for(Observer observer:observerList){
observer.update(context);
}
}
//韩非子要吃饭了
public void haveBreakfast(){
System.out.println("韩非子:开始吃饭了...");
//通知所有的观察者
this.notifyObservers("韩非子在吃饭");
}
//韩非子开始娱乐了
public void haveFun(){
System.out.println("韩非子:开始娱乐了...");

this.notifyObservers("韩非子在娱乐");
}
}

观察者只是把原有的ILiSi接口修改了一个名字而已,如代码清单22-11所示。

代码清单22-11 观察者接口

1
2
3
4
public interface Observer {
//一发现别人有动静,自己也要行动起来
public void update(String context);
}

然后是三个很无耻的观察者,咱先看看真实的李斯,如代码清单22-12所示。

代码清单22-12 具体的观察者

1
2
3
4
5
6
7
8
9
10
11
12
public class LiSi implements Observer{
//首先李斯是个观察者,一旦韩非子有活动,他就知道,他就要向老板汇报
public void update(String str){
System.out.println("李斯:观察到韩非子活动,开始向老板汇报了...");
this.reportToQinShiHuang(str);
System.out.println("李斯:汇报完毕...\n");
}
//汇报给秦始皇
private void reportToQinShiHuang(String reportContext){
System.out.println("李斯:报告,秦老板!韩非子有活动了-->"+reportContext);
}
}

李斯是真有其人,以下两个观察者王斯和刘斯是杜撰出来的,如代码清单22-13所示。

代码清单22-13 杜撰的观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class WangSi implements Observer{
//王斯,看到韩非子有活动
public void update(String str){
System.out.println("王斯:观察到韩非子活动,自己也开始活动了...");
this.cry(str);
System.out.println("王斯:哭死了...\n");
}
//一看韩非子有活动,他就痛哭
private void cry(String context){
System.out.println("王斯:因为"+context+",--所以我悲伤呀!");
}
}
public class LiuSi implements Observer{
//刘斯,观察到韩非子活动后,自己也得做一些事
public void update(String str){

System.out.println("刘斯:观察到韩非子活动,开始动作了...");
this.happy(str);
System.out.println("刘斯:乐死了\n");
}
//一看韩非子有变化,他就快乐
private void happy(String context){
System.out.println("刘斯:因为" +context+",--所以我快乐呀!" );
}
}

所有的历史人物都在场了,那我们来看看这场历史闹剧是如何演绎的,如代码清单22- 14所示。

代码清单22-14 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
public static void main(String[] args) {
//三个观察者产生出来
Observer liSi = new LiSi();
Observer wangSi = new WangSi();
Observer liuSi = new LiuSi();
//定义出韩非子
HanFeiZi hanFeiZi = new HanFeiZi();
//我们后人根据历史,描述这个场景,有三个人在观察韩非子
hanFeiZi.addObserver(liSi);
hanFeiZi.addObserver(wangSi);
hanFeiZi.addObserver(liuSi);
//然后这里我们看看韩非子在干什么
hanFeiZi.haveBreakfast();
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
韩非子:开始吃饭了... 
李斯:观察到韩非子活动,开始向老板汇报了...
李斯:报告,秦老板!韩非子有活动了--->韩非子在吃饭
李斯:汇报完毕...
王斯:观察到韩非子活动,自己也开始活动了...
王斯:因为韩非子在吃饭——所以我悲伤呀!
王斯:哭死了...
刘斯:观察到韩非子活动,开始动作了...
刘斯:因为韩非子在吃饭——所以我快乐呀!
刘斯:乐死了

好了,结果也正确了,也符合开闭原则了,同时也实现类间解耦,想再加观察者?继续实现Observer接口就成了,这时候必须修改Client程序,因为你的业务都发生了变化。这就是观察者模式。

26.1 城市的纵向发展功臣——电梯

现在城市发展很快,百万级人口的城市很多,那其中有两个东西的发明在城市的发展中起到非常重要的作用:一个是汽车,另一个是电梯。汽车让城市可以横向扩展,电梯让城市可以纵向延伸,向空中伸展。汽车对城市的发展我们就不说了,电梯,你想想看,如果没有电梯,每天你需要爬15层楼梯,你是不是会累坏了?建筑师设计了一个没有电梯的建筑,投资者肯定不愿意投资,那也是建筑师的耻辱,今天我们就用程序表现一下这个电梯是怎么运作的。

我们每天都在乘电梯,那我们来看看电梯有哪些动作(映射到Java中就是有多少方法):开门、关门、运行、停止。好,我们就用程序来实现一下电梯的动作,先看类图设计,如图26-1所示。

image-20210930105239335

图26-1 电梯的类图
非常简单的类图,定义一个接口,然后是一个实现类,然后业务场景类Client就可以调用,并运行起来,简单也要实现出来。看看该程序的接口,如代码清单26-1所示。

代码清单26-1 电梯接口

1
2
3
4
5
6
7
8
9
10
public interface ILift {
//首先电梯门开启动作
public void open();
//电梯门可以开启,那当然也就有关闭了
public void close();
//电梯要能上能下
public void run();
//电梯还要能停下来
public void stop();
}

接口有了,再来看实现类,如代码清单26-2所示。

代码清单26-2 电梯实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Lift implements ILift {
//电梯门关闭
public void close() {
System.out.println("电梯门关闭...");
}
//电梯门开启
public void open() {
System.out.println("电梯门开启...");
}
//电梯开始运行起来
public void run() {
System.out.println("电梯上下运行起来...");
}
//电梯停止
public void stop() {
System.out.println("电梯停止了...");
}
}

电梯的开、关、运行、停都实现了,再看看场景类是怎么调用的,如代码清单26-3所示。

代码清单26-3 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
ILift lift = new Lift();
//首先是电梯门开启,人进去
lift.open();
//然后电梯门关闭
lift.close();
//再然后,电梯运行起来,向上或者向下
lift.run();
//最后到达目的地,电梯停下来
lift.stop();
}
}

运行的结果如下所示:

1
2
3
4
电梯门开启... 
电梯门关闭...
电梯上下运行起来...
电梯停止了...

太简单的程序了!每个程序员都会写这个程序,这么简单的程序还拿出来显摆,是不是太小看我们的智商了?非也,非也,我们继续往下分析,这个程序有什么问题?你想想,电梯门可以打开,但不是随时都可以开,是有前提条件的。你不可能电梯在运行的时候突然开门吧?!电梯也不会出现停止了但是不开门的情况吧?!那要是有也是事故嘛,再仔细想想,电梯的这4个动作的执行都有前置条件,具体点说就是在特定状态下才能做特定事,那我们来分析一下电梯有哪些特定状态。

  • 敞门状态

按了电梯上下按钮,电梯门开,这中间大概有10秒的时间,那就是敞门状态。在这个状态下电梯只能做的动作是关门动作。

  • 闭门状态

电梯门关闭了,在这个状态下,可以进行的动作是:开门(我不想坐电梯了)、停止 (忘记按路层号了)、运行。

  • 运行状态

电梯正在跑,上下窜,在这个状态下,电梯只能做的是停止。

  • 停止状态

电梯停止不动,在这个状态下,电梯有两个可选动作:继续运行和开门动作。

我们用一张表来表示电梯状态和动作之间的关系,如图26-2所示。

image-20210930105605468

图26-2 电梯状态和动作对应表(○表示不允许,☆表示允许动作)

看到这张表后,我们才发觉,哦,我们的程序做得很不严谨,好,我们来修改一下,如图26-3所示。

在接口中定义了4个常量,分别表示电梯的4个状态:敞门状态、闭门状态、运行状态、 停止状态,然后在实现类中电梯的每一次动作发生都要对状态进行判断,判断是否可以执行,也就是动作的执行是否符合业务逻辑,实现类中有4个私有方法是仅仅实现电梯的动作,没有任何前置条件,因此这4个方法是不能为外部类调用的,设置为私有方法。我们先看接口的改变,如代码清单26-4所示。

image-20210930105659255

图26-3 增加了状态的类图

代码清单26-4 电梯接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface ILift {
//电梯的4个状态
public final static int OPENING_STATE = 1;
//敞门状态
public final static int CLOSING_STATE = 2;
//闭门状态
public final static int RUNNING_STATE = 3;
//运行状态
public final static int STOPPING_STATE = 4;
//停止状态
//设置电梯的状态
public void setState(int state);
//首先电梯门开启动作
public void open();
//电梯门可以开启,那当然也就有关闭了
public void close();
//电梯要能上能下,运行起来
public void run();
//电梯还要能停下来
public void stop();
}

这里增加了4个静态常量,并增加了一个方法setState,设置电梯的状态。我们再来看实现类是如何实现的,如代码清单26-5所示。

代码清单26-5 电梯实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
public class Lift implements ILift {
private int state;
public void setState(int state) {
this.state = state;
}
//电梯门关闭
public void close() {
//电梯在什么状态下才能关闭
switch(this.state){
case OPENING_STATE://可以关门,同时修改电梯状态
this.closeWithoutLogic();
this.setState(CLOSING_STATE);
break;
case CLOSING_STATE://电梯是关门状态,则什么都不做
//do
nothing;
break;
case RUNNING_STATE://正在运行,门本来就是关闭的,也什么都不做
//do
nothing;
break;
case STOPPING_STATE://停止状态,门也是关闭的,什么也不做
//do
nothing;
break;
}
}
//电梯门开启
public void open() {
//电梯在什么状态才能开启
switch(this.state){
case OPENING_STATE://闭门状态,什么都不做
//do
nothing;
break;
case CLOSING_STATE://闭门状态,则可以开启
this.openWithoutLogic();
this.setState(OPENING_STATE);
break;
case RUNNING_STATE://运行状态,则不能开门,什么都不做
//do
nothing;
break;
case STOPPING_STATE://停止状态,当然要开门了
this.openWithoutLogic();
this.setState(OPENING_STATE);
break;
}
}
//电梯开始运行起来
public void run() {
switch(this.state){
case OPENING_STATE://敞门状态,什么都不做
//do
nothing;
break;
case CLOSING_STATE://闭门状态,则可以运行
this.runWithoutLogic();
this.setState(RUNNING_STATE);
break;
case RUNNING_STATE://运行状态,则什么都不做
//do
nothing;
break;
case STOPPING_STATE://停止状态,可以运行
this.runWithoutLogic();
this.setState(RUNNING_STATE);
}
}
//电梯停止
public void stop() {
switch(this.state){
case OPENING_STATE://敞门状态,要先停下来的,什么都不做
//do
nothing;
break;
case CLOSING_STATE://闭门状态,则当然可以停止了
this.stopWithoutLogic();
this.setState(CLOSING_STATE);
break;
case RUNNING_STATE://运行状态,有运行当然那也就有停止了
this.stopWithoutLogic();
this.setState(CLOSING_STATE);
break;
case STOPPING_STATE://停止状态,什么都不做
//do
nothing;
break;
}
}
//纯粹的电梯关门,不考虑实际的逻辑
private void closeWithoutLogic(){
System.out.println("电梯门关闭...");
}
//纯粹的电梯开门,不考虑任何条件
private void openWithoutLogic(){
System.out.println("电梯门开启...");
}
//纯粹的运行,不考虑其他条件
private void runWithoutLogic(){
System.out.println("电梯上下运行起来...");
}
//单纯的停止,不考虑其他条件
private void stopWithoutLogic(){
System.out.println("电梯停止了...");
}
}

程序有点长,但是还是很简单的,就是在每一个接口定义的方法中使用switch…case来判断它是否符合业务逻辑,然后运行指定的动作。我们重新编写一个场景类来描述一下该环境,如代码清单26-6所示。

代码清单26-6 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) {
ILift lift = new Lift();
//电梯的初始条件应该是停止状态
lift.setState(ILift.STOPPING_STATE);
//首先是电梯门开启,人进去
lift.open();
//然后电梯门关闭
lift.close();
//再然后,电梯运行起来,向上或者向下
lift.run();
//最后到达目的地,电梯停下来
lift.stop();
}
}

在业务调用的方法中增加了电梯状态判断,电梯要不是随时都可以开的,必须满足一定条件才能开门,人才能走进去,我们设置电梯的起始是停止状态,运行结果如下所示:

1
2
3
4
电梯门开启... 
电梯门关闭...
电梯上下运行起来...
电梯停止了...

我们来想一下,这段程序有什么问题。

  • 电梯实现类Lift有点长

长的原因是我们在程序中使用了大量的switch…case这样的判断(if…else也是一样),程序中只要有这样的判断就避免不了加长程序,而且在业务复杂的情况下,程序会更长,这就不是一个很好的习惯了,较长的方法和类无法带来良好的维护性,毕竟,程序首先是给人阅读的,然后才是机器执行。

  • 扩展性非常差劲

大家来想想,电梯还有两个状态没有加,是什么?通电状态和断电状态,你要是在程序增加这两个方法,你看看Open()、Close()、Run()、Stop()这4个方法都要增加判断条件,也就是说switch判断体中还要增加case项,这与开闭原则相违背。

  • 非常规状态无法实现

我们来思考我们的业务,电梯在门敞开状态下就不能上下运行了吗?电梯有没有发生过只有运行没有停止状态呢(从40层直接坠到1层嘛)?电梯故障嘛,还有电梯在检修的时候,可以在stop状态下不开门,这也是正常的业务需求呀,你想想看,如果加上这些判断条件,上面的程序有多少需要修改?虽然这些都是电梯的业务逻辑,但是一个类有且仅有一个原因引起类的变化,单一职责原则,看看我们的类,业务任务上一个小小的增加或改动都使得我们这个电梯类产生了修改,这在项目开发上是有很大风险的。

既然我们已经发现程序中有以上问题,我们怎么来修改呢?刚刚我们是从电梯的方法以及这些方法执行的条件去分析,现在我们换个角度来看问题。我们来想,电梯在具有这些状态的时候能够做什么事情,也就是说在电梯处于某个具体状态时,我们来思考这个状态是由什么动作触发而产生的,以及在这个状态下电梯还能做什么事情。例如,电梯在停止状态时,我们来思考两个问题:

  • 停止状态是怎么来的,那当然是由于电梯执行了stop方法而来的。
  • 在停止状态下,电梯还能做什么动作?继续运行?开门?当然都可以了。

我们再来分析其他3个状态,也都是一样的结果,我们只要实现电梯在一个状态下的两个任务模型就可以了:这个状态是如何产生的,以及在这个状态下还能做什么其他动作(也就是这个状态怎么过渡到其他状态),既然我们以状态为参考模型,那我们就先定义电梯的状态接口,类图如图26-4所示。

image-20210930110024745

图26-4 以状态作为导向的类图
在类图中,定义了一个LiftState抽象类,声明了一个受保护的类型Context变量,这个是串联各个状态的封装类。封装的目的很明显,就是电梯对象内部状态的变化不被调用类知晓,也就是迪米特法则了(我的类内部情节你知道得越少越好),并且还定义了4个具体的实现类,承担的是状态的产生以及状态间的转换过渡,我们先来看LiftState代码,如代码清单26-7所示。

代码清单26-7 抽象电梯状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class LiftState{
//定义一个环境角色,也就是封装状态的变化引起的功能变化
protected Context context;
public void setContext(Context _context){
this.context = _context;
}
//首先电梯门开启动作
public abstract void open();
//电梯门有开启,那当然也就有关闭了
public abstract void close();
//电梯要能上能下,运行起来
public abstract void run();
//电梯还要能停下来
public abstract void stop();
}

抽象类比较简单,我们先看一个具体的实现——敞门状态的实现类,如代码清单26-8所示。

代码清单26-8 敞门状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class OpenningState extends LiftState {
//开启当然可以关闭了,我就想测试一下电梯门开关功能
@Override
public void close() {
//状态修改
super.context.setLiftState(Context.closeingState);
//动作委托为CloseState来执行
super.context.getLiftState().close();
}
//打开电梯门
@Override
public void open() {
System.out.println("电梯门开启...");
}
//门开着时电梯就运行跑,这电梯,吓死你!
@Override
public void run() {
//do
nothing;
}
//开门还不停止?
public void stop() {
//do
nothing;
}
}

我来解释一下这个类的几个方法,Openning状态是由open()方法产生的,因此,在这个方法中有一个具体的业务逻辑,我们是用print来代替了。在Openning状态下,电梯能过渡到其他什么状态呢?按照现在的定义的是只能过渡到Closing状态,因此我们在Close()中定义了状态变更,同时把Close这个动作也委托了给CloseState类下的Close方法执行,这个可能不好理解,我们再看看Context类可能好理解一点,如代码清单26-9所示。

代码清单26-9 上下文类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Context {
//定义出所有的电梯状态
public final static OpenningState openningState = new OpenningState();
public final static ClosingState closeingState = new ClosingState();
public final static RunningState runningState = new RunningState();
public final static StoppingState stoppingState = new StoppingState();

//定义一个当前电梯状态
private LiftState liftState;
public LiftState getLiftState() {
return liftState;
}
public void setLiftState(LiftState liftState) {
this.liftState = liftState;
//把当前的环境通知到各个实现类中
this.liftState.setContext(this);
}
public void open(){
this.liftState.open();
}
public void close(){
this.liftState.close();
}
public void run(){
this.liftState.run();
}
public void stop(){
this.liftState.stop();
}
}

结合以上3个类,我们可以这样理解:Context是一个环境角色,它的作用是串联各个状态的过渡,在LiftSate抽象类中我们定义并把这个环境角色聚合进来,并传递到子类,也就是4个具体的实现类中自己根据环境来决定如何进行状态的过渡。关闭状态如代码清单26-10所示。

代码清单26-10 关闭状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ClosingState extends LiftState {
//电梯门关闭,这是关闭状态要实现的动作
@Override
public void close() {
System.out.println("电梯门关闭...");
}
//电梯门关了再打开
@Override
public void open() {
super.context.setLiftState(Context.openningState);
//置为敞门状态
super.context.getLiftState().open();
}
//电梯门关了就运行,这是再正常不过了
@Override
public void run() {
super.context.setLiftState(Context.runningState);
//设置为运行状态
super.context.getLiftState().run();
}
//电梯门关着,我就不按楼层
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);
//设置为停止状态
super.context.getLiftState().stop();
}
}

运行状态如代码清单26-11所示。

代码清单26-11 运行状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class RunningState extends LiftState {
//电梯门关闭?这是肯定的
@Override
public void close() {
//do
nothing }
//运行的时候开电梯门?你疯了!电梯不会给你开的
@Override
public void open() {
//do
nothing }
//这是在运行状态下要实现的方法
@Override
public void run() {
System.out.println("电梯上下运行...");
}
//这绝对是合理的,只运行不停止还有谁敢坐这个电梯?!估计只有上帝了
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);
//环境设置为停止状态
super.context.getLiftState().stop();
}
}

停止状态如代码清单26-12所示。

代码清单26-12 停止状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class StoppingState extends LiftState {
//停止状态关门?电梯门本来就是关着的!
@Override
public void close() {
//do
nothing;
}
//停止状态,开门,那是要的!
@Override
public void open() {
super.context.setLiftState(Context.openningState);
super.context.getLiftState().open();
}
//停止状态再运行起来,正常得很
@Override
public void run() {
super.context.setLiftState(Context.runningState);
super.context.getLiftState().run();
}
//停止状态是怎么发生的呢?当然是停止方法执行了
@Override
public void stop() {
System.out.println("电梯停止了...");
}
}

业务逻辑都已经实现了,我们看看怎么来模拟场景类,如代码清单26-13所示。

代码清单26-13 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
Context context = new Context();
context.setLiftState(new ClosingState());
context.open();
context.close();
context.run();
context.stop();
}
}

Client场景类太简单了,只要定义一个电梯的初始状态,然后调用相关的方法,就完成 了,完全不用考虑状态的变更,运行结果完全相同,不再赘述。

我们再来回顾一下我们刚刚批判的上一段代码。首先是代码太长,这个问题已经解决了,通过各个子类来实现,每个子类的代码都很短,而且也取消了switch…case条件的判断。 其次是不符合开闭原则,那如果在我们这个例子中要增加两个状态应该怎么做呢?增加两个子类,一个是通电状态,另一个是断电状态,同时修改其他实现类的相应方法,因为状态要过渡,那当然要修改原有的类,只是在原有类中的方法上增加,而不去做修改。再次是不符合迪米特法则,我们现在的各个状态是单独的类,只有与这个状态有关的因素修改了,这个类才修改,符合迪米特法则,非常完美!这就是状态模式。