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条件的判断。 其次是不符合开闭原则,那如果在我们这个例子中要增加两个状态应该怎么做呢?增加两个子类,一个是通电状态,另一个是断电状态,同时修改其他实现类的相应方法,因为状态要过渡,那当然要修改原有的类,只是在原有类中的方法上增加,而不去做修改。再次是不符合迪米特法则,我们现在的各个状态是单独的类,只有与这个状态有关的因素修改了,这个类才修改,符合迪米特法则,非常完美!这就是状态模式。

30.2 抽象工厂模式VS建造者模式

抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。而建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品,两者的区别还是比较明显的,但是还有读者对这两个模式产生混淆,我们通过一个例子说明两者的差别。

现代化的汽车工厂能够批量生产汽车(不考虑手工打造的豪华车)。不同的工厂生产不同的汽车,宝马工厂生产宝马牌子的车,奔驰工厂生产奔驰牌子的车。车不仅具有不同品牌,还有不同的用途分类,如商务车Van,运动型车SUV等,我们按照两种设计模式分别实现车辆的生产过程。

30.2.1 按抽象工厂模式生产车辆

按照抽象工厂模式,首先需要定义一个抽象的产品接口即汽车接口,然后宝马和奔驰分别实现该接口,由于它们只具有了一个品牌属性,还没有定义一个具体的型号,属于对象的抽象层次,每个具体车型由其子类实现,如R系列的奔驰车是商务车,X系列的宝马车属于SUV,我们来看类图,如图30-3所示。

image-20210930155636695

图30-3 车辆生产的工厂类图

在类图中,产品类很简单,我们从两个维度看产品:品牌和车型,每个品牌下都有两个车型,如宝马SUV,宝马商务车等,同时我们又建造了两个工厂,一个专门生产宝马车的宝马工厂BMWFactory,一个是生产奔驰车的奔驰车生产工厂BenzFactory。当然,汽车工厂也有两个不同的维度,可以建立这样两个工厂:一个专门生产SUV车辆的生产工厂,生产宝马SUV和奔驰SUV,另外一个工厂专门生成商务车,分别是宝马商务车和奔驰商务车,这样设计在技术上是完全可行的,但是在业务上是不可行的,为什么?这是因为你看到过有一个工厂既能生产奔驰SUV也能生产宝马SUV吗?这是不可能的,因为业务受限,除非是国内的山寨工厂。我们先来看产品类,汽车接口如代码清单30-12所示。

代码清单30-12 汽车接口

1
2
3
4
5
6
public interface ICar {
//汽车的生产商,也就是牌子
public String getBand();
//汽车的型号
public String getModel();
}

在产品接口中我们定义了车辆有两个可以查询的属性:品牌和型号,奔驰车和宝马车是两个不同品牌的产品,但不够具体,只是知道它们的品牌而已,还不能够实例化,因此还是一个抽象类,如代码清单30-13所示。

代码清单30-13 抽象宝马车

1
2
3
4
5
6
7
8
9
public abstract class AbsBMW implements ICar {
private final static String BMW_BAND = "宝马汽车";
//宝马车
public String getBand() {
return BMW_BAND;
}
//型号由具体的实现类实现
public abstract String getModel();
}

抽象产品类中实现了产品的类型定义,车辆的型号没有实现,两实现类分别实现商务车和运动型车,分别如代码清单30-14、代码清单30-15所示。

代码清单30-14 宝马商务车

1
2
3
4
5
6
public class BMWVan extends AbsBMW {
private final static String SEVENT_SEARIES = "7系列车型商务车";
public String getModel() {
return SEVENT_SEARIES;
}
}

代码清单30-15 宝马SUV

1
2
3
4
5
6
public class BMWSuv extends AbsBMW {
private final static String X_SEARIES = "X系列车型SUV";
public String getModel() {
return X_SEARIES;
}
}

奔驰车与宝马车类似,都已经有清晰品牌定义,但是型号还没有确认,也是一个抽象的产品类,如代码清单30-16所示。

代码清单30-16 抽象奔驰车

1
2
3
4
5
6
7
8
public abstract class AbsBenz implements ICar {
private final static String BENZ_BAND = "奔驰汽车";
public String getBand() {
return BENZ_BAND;
}
//具体型号由实现类完成
public abstract String getModel();
}

由于分类的标准是相同的,因此奔驰车也应该有商务车和运动车两个类型,分别如代码清单30-17和代码清单30-18所示。

代码清单30-17 奔驰商务车

1
2
3
4
5
6
public class BenzVan extends AbsBenz {
private final static String R_SERIES = "R系列商务车";
public String getModel() {
return R_SERIES;
}
}

代码清单30-18 奔驰SUV

1
2
3
4
5
6
public class BenzSuv extends AbsBenz {
private final static String G_SERIES = "G系列SUV";
public String getModel() {
return G_SERIES;
}
}

所有的产品类都已经实现了,剩下的工作就是要定义工厂类进行生产,由于产品类型多样,也导致了必须有多个工厂类来生产不同产品,首先就需要定义一个抽象工厂,声明每个工厂必须完成的职责,如代码清单30-19所示。

代码清单30-19 抽象工厂

1
2
3
4
5
6
public interface CarFactory {
//生产SUV
public ICar createSuv();
//生产商务车
public ICar createVan();
}

抽象工厂定义了每个工厂必须生产两个类型车:SUV(运动车)和VAN(商务车),否则一个工厂就不能被实例化,我们来看宝马车工厂,如代码清单30-20所示。

代码清单30-20 宝马车工厂

1
2
3
4
5
6
7
8
9
10
public class BMWFactory implements CarFactory {
//生产SUV
public ICar createSuv() {
return new BMWSuv();
}
//生产商务车
public ICar createVan(){
return new BMWVan();
}
}

很简单,你要我生产宝马商务车,没问题,直接产生一个宝马商务车对象,返回给调用者,这对调用者来说根本不需要关心到底是怎么生产的,它只要找到一个宝马工厂,即可生产出自己需要的产品(汽车)。奔驰车工厂与此类似,如代码清单30-21所示。

代码清单30-21 奔驰车工厂

1
2
3
4
5
6
7
8
9
10
public class BenzFactory implements CarFactory {
//生产SUV
public ICar createSuv() {
return new BenzSuv();
}
//生产商务车
public ICar createVan(){
return new BenzVan();
}
}

产品和工厂都具备了,剩下的工作就是建立一个场景类模拟调用者调用,如代码清单30-22所示。

代码清单30-22 场景类

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) {
//要求生产一辆奔驰SUV
System.out.println("===要求生产一辆奔驰SUV===");
//首先找到生产奔驰车的工厂
System.out.println("A、找到奔驰车工厂");
CarFactory carFactory= new BenzFactory();
//开始生产奔驰SUV
System.out.println("B、开始生产奔驰SUV");
ICar benzSuv = carFactory.createSuv();
//生产完毕,展示一下车辆信息
System.out.println("C、生产出的汽车如下:");
System.out.println("汽车品牌:"+benzSuv.getBand());
System.out.println("汽车型号:" + benzSuv.getModel());
}
}

运行结果如下所示:

1
2
3
4
5
6
===要求生产一辆奔驰SUV=== 
A、找到奔驰车工厂
B、开始生产奔驰SUV
C、生产出的汽车如下:
汽车品牌:奔驰汽车
汽车型号:G系列SUV

对外界调用者来说,只要更换一个具备相同结构的对象,即可发生非常大的改变,如我们原本使用BenzFactory生产汽车,但是过了一段时间后,我们的系统需要生产宝马汽车,这对系统来说不需要很大的改动,只要把工厂类使用BMWFactory代替即可,立刻可以生产出宝马车,注意这里生产的是一辆完整的车,对于一个产品,只要给出产品代码(车类型)即可生产,抽象工厂模式把一辆车认为是一个完整的、不可拆分的对象。它注重完整性,一个产品一旦找到一个工厂生产,那就是固定的型号,不会出现一个宝马工厂生产奔驰车的情况。那现在的问题是我们就想要一辆混合的车型,如奔驰的引擎,宝马的车轮,那该怎么处理呢?使用我们的建造者模式!

30.2.2 按建造者模式生产车辆

按照建造者模式设计一个生产车辆需要把车辆进行拆分,拆分成引擎和车轮两部分,然后由建造者进行建造,想要什么车,你只要有设计图纸就成,马上可以制造一辆车出来。它注重的是对零件的装配、组合、封装,它从一个细微构件装配角度看待一个对象。我们来看生产车辆的类图,如图30-4所示。

注意看我们类图中的蓝图类Blueprint,它负责对产品建造过程定义。既然要生产产品, 那必然要对产品进行一个描述,在类图中我们定义了一个接口来描述汽车,如代码清单30- 23所示。

代码清单30-23 车辆产品描述

1
2
3
4
5
6
public interface ICar {
//汽车车轮
public String getWheel();
//汽车引擎
public String getEngine();
}

image-20210930160246938

图30-4 建造者模式建造车辆

我们定义一辆车必须有车轮和引擎,具体的产品如代码清单30-24所示。

代码清单30-24 具体车辆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Car implements ICar {
//汽车引擎
private String engine;
//汽车车轮
private String wheel;
//一次性传递汽车需要的信息
public Car(String _engine,String _wheel){
this.engine = _engine;
this.wheel = _wheel;
}
public String getEngine() {
return engine;
}
public String getWheel() {
return wheel;
}
public String toString(){
return "车的轮子是:" + wheel + "\n车的引擎是:" + engine;
}
}

一个简单的JavaBean定义产品的属性,明确对产品的描述。我们继续来思考,因为我们的产品是比较抽象的,它没有指定引擎的型号,也没有指定车轮的牌子,那么这样的组合方式有很多,完全要靠建造者来建造,建造者说要生产一辆奔驰SUV那就得用奔驰的引擎和奔驰的车轮,该建造者对于一个具体的产品来说是绝对的权威,我们来描述一下建造者,如代码清单30-25所示。

代码清单30-25 抽象建造者

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 CarBuilder {
//待建造的汽车
private ICar car;
//设计蓝图
private Blueprint bp;
public Car buildCar(){
//按照顺序生产一辆车
return new Car(buildEngine(),buildWheel());
}
//接收一份设计蓝图
public void receiveBlueprint(Blueprint _bp){
this.bp = _bp;
}
//查看蓝图,只有真正的建造者才可以查看蓝图
protected Blueprint getBlueprint(){
return bp;
}
//建造车轮
protected abstract String buildWheel();
//建造引擎
protected abstract String buildEngine();
}

看到Blueprint类了,它中文的意思是“蓝图”,你要建造一辆车必须有一个设计样稿或者蓝图吧,否则怎么生产?怎么装配?该类就是一个可参考的生产样本,如代码清单30-26所示。

代码清单30-26 生产蓝图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Blueprint {
//车轮的要求
private String wheel;
//引擎的要求
private String engine;
public String getWheel() {
return wheel;
}
public void setWheel(String wheel) {
this.wheel = wheel;
}
public String getEngine() {
return engine;
}
public void setEngine(String engine) {
this.engine = engine;
}
}

这和一个具体的产品Car类是一样的?错,不一样!它是一个蓝图,是一个可以参考的模板,有一个蓝图可以设计出非常多的产品,如有一个R系统的奔驰商务车设计蓝图,我们就可以生产出一系列的奔驰车。它指导我们的产品生产,而不是一个具体的产品。我们来看宝马车建造车间,如代码清单30-27所示。

代码清单30-27 宝马车建造车间

1
2
3
4
5
6
7
8
public class BMWBuilder extends CarBuilder {
public String buildEngine() {
return super.getBlueprint().getEngine();
}
public String buildWheel() {
return super.getBlueprint().getWheel();
}
}

这是非常简单的类。只要获得一个蓝图,然后按照蓝图制造引擎和车轮即可,剩下的事情就交给抽象的建造者进行装配。奔驰车间与此类似,如代码清单30-28所示。

代码清单30-28 奔驰车建造车间

1
2
3
4
5
6
7
8
public class BenzBuilder extends CarBuilder {
public String buildEngine() {
return super.getBlueprint().getEngine();
}
public String buildWheel() {
return super.getBlueprint().getWheel();
}
}

两个建造车间都已经完成,那现在的问题就变成了怎么让车间运作,谁来编写蓝图?谁来协调生产车间?谁来对外提供最终产品?于是导演类出场了,它不仅仅有每个车间需要的设计蓝图,还具有指导不同车间装配顺序的职责,如代码清单30-29所示。

代码清单30-29 导演类

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 Director {
//声明对建造者的引用
private CarBuilder benzBuilder = new BenzBuilder();
private CarBuilder bmwBuilder = new BMWBuilder();
//生产奔驰SUV
public ICar createBenzSuv(){
//制造出汽车
return createCar(benzBuilder, "benz的引擎", "benz的轮胎");
}
//生产出一辆宝马商务车
public ICar createBMWVan(){
return createCar(benzBuilder, "BMW的引擎", "BMW的轮胎");
}
//生产出一个混合车型
public ICar createComplexCar(){
return createCar(bmwBuilder, "BMW的引擎", "benz的轮胎");
}
//生产车辆
private ICar createCar(CarBuilder _carBuilder,String engine,String wheel){
//导演怀揣蓝图
Blueprint bp = new Blueprint();
bp.setEngine(engine);
bp.setWheel(wheel);
System.out.println("获得生产蓝图");
_carBuilder.receiveBlueprint(bp);
return _carBuilder.buildCar();
}
}

这里有一个私有方法createCar,其作用是减少导演类中的方法对蓝图的依赖,全部由该方法来完成。我们编写一个场景类,如代码清单30-30所示。

代码清单30-30 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Client {
public static void main(String[] args) {
//定义出导演类
Director director =new Director();
//给我一辆奔驰车SUV
System.out.println("===制造一辆奔驰SUV===");

ICar benzSuv = director.createBenzSuv();
System.out.println(benzSuv);
//给我一辆宝马商务车
System.out.println("\n===制造一辆宝马商务车===");
ICar bmwVan = director.createBMWVan();
System.out.println(bmwVan);
//给我一辆混合车型
System.out.println("\n===制造一辆混合车===");
ICar complexCar = director.createComplexCar();
System.out.println(complexCar);
}
}

场景类只要找到导演类(也就是车间主任了)说给我制造一辆这样的宝马车,车间主任马上通晓你的意图,设计了一个蓝图,然后命令建造车间拼命加班加点建造,最终返回给你一件最新出品的产品,运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
===制造一辆奔驰SUV=== 
获得生产蓝图
车的轮子是:benz的轮胎
车的引擎是:benz的引擎
===制造一辆宝马商务车===
获得生产蓝图
车的轮子是:BMW的轮胎
车的引擎是:BMW的引擎
===制造一辆混合车===
获得生产蓝图
车的轮子是:benz的轮胎
车的引擎是:BMW的引擎

注意最后一个运行结果片段,我们可以立刻生产出一辆混合车型,只要有设计蓝图,这非常容易实现。反观我们的抽象工厂模式,它是不可能实现该功能的,因为它更关注的是整体,而不关注到底用的是奔驰引擎还是宝马引擎,而我们的建造者模式却可以很容易地实现该设计,市场信息变更了,我们就可以立刻跟进,生产出客户需要的产品。

30.2.3 最佳实践

注意看上面的描述,我们在抽象工厂模式中使用“工厂”来描述构建者,而在建造者模式中使用“车间”来描述构建者,其实我们已经在说它们两者的区别了,抽象工厂模式就好比是一个一个的工厂,宝马车工厂生产宝马SUV和宝马VAN,奔驰车工厂生产奔驰车SUV和奔驰VAN,它是从一个更高层次去看对象的构建,具体到工厂内部还有很多的车间,如制造引擎的车间、装配引擎的车间等,但这些都是隐藏在工厂内部的细节,对外不公布。也就是对领导者来说,他只要关心一个工厂到底是生产什么产品的,不用关心具体怎么生产。而建造者模式就不同了,它是由车间组成,不同的车间完成不同的创建和装配任务,一个完整的汽车生产过程需要引擎制造车间、引擎装配车间的配合才能完成,它们配合的基础就是设计蓝图,而这个蓝图是掌握在车间主任(导演类)手中,它给建造车间什么蓝图就能生产什么产品,建造者模式更关心建造过程。虽然从外界看来一个车间还是生产车辆,但是这个车间的转型是非常快的,只要重新设计一个蓝图,即可产生不同的产品,这有赖于建造者模式的功劳。

相对来说,抽象工厂模式比建造者模式的尺度要大,它关注产品整体,而建造者模式关注构建过程,因此建造者模式可以很容易地构建出一个崭新的产品,只要导演类能够提供具体的工艺流程。也正因为如此,两者的应用场景截然不同,如果希望屏蔽对象的创建过程, 只提供一个封装良好的对象,则可以选择抽象工厂方法模式。而建造者模式可以用在构件的装配方面,如通过装配不同的组件或者相同组件的不同顺序,可以产生出一个新的对象,它可以产生一个非常灵活的架构,方便地扩展和维护系统。

32.1 命令模式VS策略模式

命令模式和策略模式的类图确实很相似,只是命令模式多了一个接收者(Receiver)角色。它们虽然同为行为类模式,但是两者的区别还是很明显的。策略模式的意图是封装算法,它认为“算法”已经是一个完整的、不可拆分的原子业务(注意这里是原子业务,而不是原子对象),即其意图是让这些算法独立,并且可以相互替换,让行为的变化独立于拥有行为的客户;而命令模式则是对动作的解耦,把一个动作的执行分为执行对象(接收者角色)、执行行为(命令角色),让两者相互独立而不相互影响。

我们从一个相同的业务需求出发,按照命令模式和策略模式分别设计出一套实现,来看看它们的侧重点有什么不同。zip和gzip文件格式相信大家都很熟悉,它们是两种不同的压缩格式,我们今天就来对一个目录或文件实现两种不同的压缩方式:zip压缩和gzip压缩(这里的压缩指的是压缩和解压缩两种对应的操作行为,下同)。实现这两种压缩格式有什么意义呢?有意义!一是zip格式(.zip后缀)是Windows操作系统常用的压缩格式,gzip格式(.gz 后缀)是*nix系统常用的压缩格式;二是JDK提供了对zip和gzip文件的操作包,非常容易实现文件的压缩和解压缩操作。

下面我们来实现不同格式的压缩和解压缩功能。

32.1.1 策略模式实现压缩算法

使用策略模式实现压缩算法非常简单,也是非常标准的,类图如图32-1所示。

在类图中,我们的侧重点是zip压缩算法和gzip压缩算法可以互相替换,一个文件或者目录可以使用zip压缩,也可以使用gzip压缩,选择哪种压缩算法是由高层模块(实际操作者) 决定的。我们来看一下代码实现。先看抽象的压缩算法,如代码清单32-1所示。

image-20210930163955571

图32-1 策略模式实现压缩算法的类图

代码清单32-1 抽象压缩算法

1
2
3
4
5
6
public interface Algorithm {
//压缩算法
public boolean compress(String source,String to);
//解压缩算法
public boolean uncompress(String source,String to);
}

每一个算法要实现两个功能:压缩和解压缩,传递进来一个绝对路径source,compress 把它压缩到to目录下,uncompress则进行反向操作——解压缩,这两个方法一定要成对地实现,为什么呢?用gzip解压缩算法能解开zip格式的压缩文件吗?我们分别来看两种不同格式的压缩算法,zip、gzip压缩算法分别如代码清单32-2、代码清单32-3所示。

代码清单32-2 zip压缩算法

1
2
3
4
5
6
7
8
9
10
11
12
public class Zip implements Algorithm {
//zip格式的压缩算法
public boolean compress(String source, String to) {
System.out.println(source + " --> " +to + " ZIP压缩成功!");
return true;
}
//zip格式的解压缩算法
public boolean uncompress(String source,String to){
System.out.println(source + " --> " +to + " ZIP解压缩成功!");
return true;
}
}

代码清单32-3 gzip压缩算法

1
2
3
4
5
6
7
8
9
10
11
12
public class Gzip implements Algorithm {
//gzip的压缩算法
public boolean compress(String source, String to) {
System.out.println(source + " --> " +to + " GZIP压缩成功!");
return true;
}
//gzip解压缩算法
public boolean uncompress(String source,String to){
System.out.println(source + " --> " +to + " GZIP解压缩成功!");
return true;
}
}

这两种压缩算法实现起来都很简单,Java对此都提供了相关的API操作,这里就不再提供详细的编写代码,读者可以参考JDK自己进行实现,或者上网搜索一下,网上有太多类似的源代码。

两个具体的算法实现了同一个接口,完全遵循依赖倒转原则。我们再来看环境角色,如代码清单32-4所示。

代码清单32-4 环境角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Context {
//指向抽象算法
private Algorithm al;
//构造函数传递具体的算法
public Context(Algorithm _al){
this.al = _al;
}
//执行压缩算法
public boolean compress(String source,String to){
return al.compress(source, to);
}
//执行解压缩算法
public boolean uncompress(String source,String to){
return al.uncompress(source, to);
}
}

也是非常简单,指定一个算法,执行该算法,一个标准的策略模式就编写完毕了。请读者注意,这里虽然有两个算法Zip和Gzip,但是对调用者来说,这两个算法没有本质上的区别,只是“形式”上不同,什么意思呢?从调用者来看,使用哪一个算法都无所谓,两者完全可以互换,甚至用一个算法替代另外一个算法。我们继续看调用者是如何调用的,如代码清单32-5所示。

代码清单32-5 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Client {
public static void main(String[] args) {
//定义环境角色
Context context;
//对文件执行zip压缩算法
System.out.println("========执行算法========");
context = new Context(new Zip());
/*
*算法替换
* context = new Context(new Gzip());
*
*/
//执行压缩算法
context.compress("c:\\windows","d:\\windows.zip");
//执行解压缩算法
context.uncompress("c:\\windows.zip","d:\\windows");
}
}

运行结果如下所示:

1
2
3
========执行算法======== 
c:\windows --> d:\windows.zip ZIP 压缩成功!
c:\windows.zip --> d:\windows ZIP 解压缩成功!

要使用gzip算法吗?在客户端(Client)上把注释删掉就可以了,其他的模块根本不受任何影响,策略模式关心的是算法是否可以相互替换。策略模式虽然简单,但是在项目组使用得非常多,可以说随手拈来就是一个策略模式。

32.1.2 命令模式实现压缩算法

命令模式的主旨是封装命令,使请求者与实现者解耦。例如,到饭店点菜,客人(请求者)通过服务员(调用者)向厨师(接收者)发送了订单(行为的请求),该例子就是通过封装命令来使请求者和接收者解耦。我们继续来看压缩和解压缩的例子,怎么使用命令模式来完成该需求呢?我们先画出类图,如图32-2所示。

类图看着复杂,但是还是一个典型的命令模式,通过定义具体命令完成文件的压缩、解压缩任务,注意我们这里对文件的每一个操作都是封装好的命令,对于给定的请求,命令不同,处理的结果当然也不同,这就是命令模式要强调的。我们先来看抽象命令,如代码清单32-6所示。

代码清单32-6 抽象压缩命令

1
2
3
4
5
6
7
public abstract class AbstractCmd {
//对接收者的引用
protected IReceiver zip = new ZipReceiver();
protected IReceiver gzip = new GzipReceiver();
//抽象方法,命令的具体单元
public abstract boolean execute(String source,String to);
}

image-20210930164353477

图32-2 命令模式实现压缩算法的类图

抽象命令定义了两个接收者的引用:zip接收者和gzip接收者,大家可以想象一下这两个“受气包”,它们完全是受众,人家让它干啥它就干啥,具体使用哪个接收者是命令决定的。具体命令有4个:zip压缩、zip解压缩、gzip压缩、gzip解压缩,分别如代码清单32-7、 32-8、32-9、32-10所示。

代码清单32-7 zip压缩命令

1
2
3
4
5
public class ZipCompressCmd extends AbstractCmd {
public boolean execute(String source,String to) {
return super.zip.compress(source, to);
}
}

代码清单32-8 zip解压缩命令

1
2
3
4
5
public class ZipUncompressCmd extends AbstractCmd {
public boolean execute(String source,String to) {
return super.zip.uncompress(source, to);
}
}

代码清单32-9 gzip压缩命令

1
2
3
4
5
public class GzipCompressCmd extends AbstractCmd {
public boolean execute(String source,String to) {
return super.gzip.compress(source, to);
}
}

代码清单32-10 gzip解压缩命令

1
2
3
4
5
public class GzipUncompressCmd extends AbstractCmd {
public boolean execute(String source,String to) {
return super.gzip.uncompress(source, to);
}
}

它们非常简单,都只有一个方法,坚决地执行命令,使用了委托的方式,由接收者来实现。我们再来看抽象接收者,如代码清单32-11所示。

代码清单32-11 抽象接收者

1
2
3
4
5
6
public interface IReceiver {
//压缩
public boolean compress(String source,String to);
//解压缩
public boolean uncompress(String source,String to);
}

抽象接收者与策略模式的抽象策略完全相同,具体的实现也完全相同,只是类名做了改动,我们先来看zip压缩的实现,如代码清单32-12所示。
代码清单32-12 zip接收者

1
2
3
4
5
6
7
8
9
10
11
12
public class ZipReceiver implements IReceiver {
//zip格式的压缩算法
public boolean compress(String source, String to) {
System.out.println(source + " --> " +to + " ZIP压缩成功!");
return true;
}
//zip格式的解压缩算法
public boolean uncompress(String source,String to){
System.out.println(source + " --> " +to + " ZIP解压缩成功!");
return true;
}
}

这就是一个具体动作执行者,它在策略模式中是一个具体的算法,关心的是是否可以被替换;而在命令模式中,它则是一个具体、真实的命令执行者。我们再来看gzip接收者,如代码清单32-13所示。

代码清单32-13 gzip接收者

1
2
3
4
5
6
7
8
9
10
11
12
public class GzipReceiver implements IReceiver {
//gzip的压缩算法
public boolean compress(String source, String to) {
System.out.println(source + " --> " +to + " GZIP压缩成功!");
return true;
}
//gzip解压缩算法
public boolean uncompress(String source,String to){
System.out.println(source + " --> " +to + " GZIP解压缩成功!");
return true;
}
}

大家可以这样思考这个问题,接收者就是厨房的厨师,具体要哪个厨师做这道菜则是餐馆的规章制度已经明确的,你让专做粤菜的师傅做一个剁椒鱼头,能做出好菜吗?在命令模式中,就是在抽象命令中定义了接收者的引用,然后在具体的实现类中确定要让哪个接收者进行处理。这就好比是客人点菜:我要一个剁椒鱼头,这就是一个命令,然后服务员 (Inovker)接收到这个命令后,就开始执行,把这个命令指定给具体的执行者执行。

当然了,接收者这部分还可以这样设计,即按照职责设计接收者,比如压缩接收者、解压缩接收者,但接口需要稍稍改动,如代码清单32-14所示。

代码清单32-14 依照职责设计的接收者接口

1
2
3
4
5
6
public interface IReceiver {
//执行zip命令
public boolean zipExec(String source,String to);
//执行gzip命令
public boolean gzipExec(String source,String to);
}

接收者接口只是定义了每个接收者都必须完成zip和gzip相关的两个逻辑,有多少个职责就有多少个实现类。我们这里只有两个职责:压缩和解压缩,分别如代码清单32-15、32-16 所示。

代码清单32-15 压缩接收者

1
2
3
4
5
6
7
8
9
10
11
12
public class CompressReceiver implements IReceiver {
//执行gzip压缩命令
public boolean gzipExec(String source, String to) {
System.out.println(source + " --> " +to + " GZIP压缩成功!");
return true;
}
//执行zip压缩命令
public boolean zipExec(String source, String to) {
System.out.println(source + " --> " +to + " ZIP压缩成功!");
return true;
}
}

代码清单32-16 解压缩接收者

1
2
3
4
5
6
7
8
9
10
11
12
public class UncompressReceiver implements IReceiver {
//执行gzip解压缩命令
public boolean gzipExec(String source, String to) {
System.out.println(source + " --> " +to + " GZIP解压缩成功!");
return true;
}
//执行zip解压缩命令
public boolean zipExec(String source, String to) {
System.out.println(source + " --> " +to + " ZIP解压缩成功!");
return true;
}
}

剩下的工作就是对抽象命令、具体命令稍作修改,这里不再赘述。为什么要在这里增加一个分支描述呢?这是为了与策略模式对比,在命令模式中,我们可以把接收者设计得与策略模式的算法相同,也可以不相同。我们按照职责设计的接口就不适用于策略模式,不可能封装一个叫做压缩的算法类,然后在类中提供两种不同格式的压缩功能,这违背了策略模式的意图——封装算法,为什么呢?如果要增加一个rar压缩算法,该怎么办呢?修改抽象算法?这是绝对不允许的!那为什么命令模式就是允许的呢?因为命令模式着重于请求者和接收者解耦,你管我接收者怎么变化,只要不影响请求者就成,这才是命令模式的意图。

命令、接收者都具备了,我们再来封装一个命令的调用者,如代码清单32-17所示。

代码清单32-17 调用者

1
2
3
4
5
6
7
8
9
10
11
public class Invoker {
//抽象命令的引用
private AbstractCmd cmd;
public Invoker(AbstractCmd _cmd){
this.cmd = _cmd;
}
//执行命令
public boolean execute(String source,String to){
return cmd.execute(source, to);
}
}

调用者非常简单,只负责把命令向后传递,当然这里也可以进行一定的拦截处理,我们暂时用不到就不做处理了。我们来看场景类是如何描述这个场景的,如代码清单32-18所示。

代码清单32-18 场景类

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) {
//定义一个命令,压缩一个文件
AbstractCmd cmd = new ZipCompressCmd();

/*
* 想换一个?执行解压命令
* AbstractCmd cmd = new ZipUncompressCmd();
*/
//定义调用者
Invoker invoker = new Invoker(cmd);
//我命令你对这个文件进行压缩
System.out.println("========执行压缩命令========");
invoker.execute("c:\\windows", "d:\\windows.zip");
}
}

想新增一个命令?当然没有问题,只要重新定义一个命令就成,命令改变了,高层模块只要调用它就成。请注意,这里的程序还有点欠缺,没有与文件的后缀名绑定,不应该出现使用zip压缩命令产生一个.gzip后缀的文件名,读者在实际应用中可以考虑与文件后缀名之间建立关联。

通过以上例子,我们看到命令模式也实现了文件的压缩、解压缩的功能,它的实现是关注了命令的封装,是请求者与执行者彻底分开,看看我们的程序,执行者根本就不用了解命令的具体执行者,它只要封装一个命令——“给我用zip格式压缩这个文件”就可以了,具体由谁来执行,则由调用者负责,如此设计后,就可以保证请求者和执行者之间可以相互独立, 各自发展而不相互影响。

同时,由于是一个命令模式,接收者的处理可以进行排队处理,在排队处理的过程中, 可以进行撤销处理,比如客人点了一个菜,厨师还没来得及做,那要撤回很简单,撤回也是命令,这是策略模式所不能实现的。

32.1.3 小结

策略模式和命令模式相似,特别是命令模式退化时,比如无接收者(接收者非常简单或者接收者是一个Java的基础操作,无需专门编写一个接收者),在这种情况下,命令模式和策略模式的类图完全一样,代码实现也比较类似,但是两者还是有区别的。

  • 关注点不同

策略模式关注的是算法替换的问题,一个新的算法投产,旧算法退休,或者提供多种算法由调用者自己选择使用,算法的自由更替是它实现的要点。换句话说,策略模式关注的是算法的完整性、封装性,只有具备了这两个条件才能保证其可以自由切换。

命令模式则关注的是解耦问题,如何让请求者和执行者解耦是它需要首先解决的,解耦的要求就是把请求的内容封装为一个一个的命令,由接收者执行。由于封装成了命令,就同时可以对命令进行多种处理,例如撤销、记录等。

  • 角色功能不同

在我们的例子中,策略模式中的抽象算法和具体算法与命令模式的接收者非常相似,但是它们的职责不同。策略模式中的具体算法是负责一个完整算法逻辑,它是不可再拆分的原子业务单元,一旦变更就是对算法整体的变更。

而命令模式则不同,它关注命令的实现,也就是功能的实现。例如我们在分支中也提到接收者的变更问题,它只影响到命令族的变更,对请求者没有任何影响,从这方面来说,接收者对命令负责,而与请求者无关。命令模式中的接收者只要符合六大设计原则,完全不用关心它是否完成了一个具体逻辑,它的影响范围也仅仅是抽象命令和具体命令,对它的修改不会扩散到模式外的模块。

当然,如果在命令模式中需要指定接收者,则需要考虑接收者的变化和封装,例如一个老顾客每次吃饭都点同一个厨师的饭菜,那就必须考虑接收者的抽象化问题。

  • 使用场景不同

策略模式适用于算法要求变换的场景,而命令模式适用于解耦两个有紧耦合关系的对象场合或者多命令多撤销的场景。