29.1 我有一个梦想……

我们每个人都有理想,但不要只是空想,理想是要靠今天的拼搏来实现的。今天咱们就来谈谈自己的理想,如希望成为一个富翁,身价过亿,有两家大公司,一家是房地产公司, 另一家是服装制造公司。这两家公司都很赚钱,天天帮你累积财富。其实你并不关心公司的类型,你关心的是它们是不是在赚钱,赚了多少,这才是你关注的。商人嘛,唯利是图是其本性,偷税漏税是方法,欺上瞒下、压榨员工血汗是常用的手段,先用类图表示一下这两个公司,如图29-1所示。

image-20210930145552835

图29-1 盈利模式的类图
类图很简单,声明了一个Corp抽象类,定义一个公司的抽象模型,公司首要是赚钱的,做义务或善举那也是有背后利益支撑的,还是赞成这句话“天下熙熙,皆为利来;天下攘攘,皆为利往”。我们先看Corp类的源代码,如代码清单29-1所示。

代码清单29-1 抽象公司

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class Corp {
/*
* 如果是公司就应该有生产,不管是软件公司还是制造业公司
* 每家公司生产的东西都不一样,所以由实现类来完成
*/
protected abstract void produce();
/** 有产品了,那肯定要销售啊,不销售公司怎么生存 */
protected abstract void sell();
//公司是干什么的?赚钱的
public void makeMoney(){
//每个公司都是一样,先生产
this.produce();
//然后销售
this.sell();
}
}

怎么这是模板方法模式啊?是的,这是个引子,请继续往下看。合适的方法存在合适的类中,这个基本上是每本Java基础书上都会讲的,但是到实际的项目中应用的时候就不是这么回事儿了。我们继续看两个实现类是如何实现的,先看HouseCorp类,这是最赚钱的公司,如代码清单29-2所示。

代码清单29-2 房地产公司

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HouseCorp extends Corp {
//房地产公司盖房子
protected void produce() {
System.out.println("房地产公司盖房子...");
}
//房地产公司卖房子,自己住那可不赚钱
protected void sell() {
System.out.println("房地产公司出售房子...");
}
//房地产公司很High了,赚钱,计算利润
public void makeMoney(){
super.makeMoney();
System.out.println("房地产公司赚大钱了...");
}
}

房地产公司按照正规翻译来说应该是realty corp,这个是比较准确的翻译,但是我问你把房地产公司翻译成英文,你的第一反应是什么?house corp!这是中式英语。我们再来看服装公司,虽然不景气,但好歹也是赚钱的,如代码清单29-3所示。

代码清单29-3 服装公司

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ClothesCorp extends Corp {
//服装公司生产的就是衣服了
protected void produce() {
System.out.println("服装公司生产衣服...");
}
//服装公司卖服装,可只卖服装,不卖穿衣服的模特
protected void sell() {
System.out.println("服装公司出售衣服...");
}
//服装公司不景气,但怎么说也是赚钱行业
public void makeMoney(){
super.makeMoney();
System.out.println("服装公司赚小钱...");
}
}

两个公司都有了,那肯定有人会关心两个公司的运营情况。你也要知道它是生产什么的,以及赚多少钱吧。通过场景类来进行模拟,如代码清单29-4所示。

代码清单29-4 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
System.out.println("-------房地产公司是这样运行的-------");
//先找到我的公司
HouseCorp houseCorp =new HouseCorp();
//看我怎么挣钱
houseCorp.makeMoney();
System.out.println("\n");
System.out.println("-------服装公司是这样运行的-------");
ClothesCorp clothesCorp = new ClothesCorp();
clothesCorp.makeMoney();
}
}

这段代码很简单,运行结果如下所示:

1
2
3
4
5
6
7
8
-------房地产公司是这样运行的------- 
房地产公司盖房子...
房地产公司出售房子...
房地产公司赚大钱了...
-------服装公司是这样运行的-------
服装公司生产衣服...
服装公司出售衣服...
服装公司赚小钱...

上述代码完全可以描述我现在的公司,但是你要知道万物都是运动的,你要用运动的眼光看问题,公司才会发展……终于有一天你觉得赚钱速度太慢,于是你上下疏通,左右打关系,终于开辟了一条赚钱的“康庄大道”:生产山寨产品!什么产品呢?即市场上什么牌子的东西火爆我生产什么牌子的东西,不管是打火机还是电脑,只要它火爆,我就生产,赚过了高峰期就换个产品,打一枪换一个牌子,不承担售后成本、也不担心销路问题,我只要正品的十分之一的价格,你买不买?哈哈,赚钱啊!

企业的方向定下来了,通过调查,苹果公司的iPod系列产品比较火爆,那咱就生产这个,把服装厂改成iPod生产厂,看类图的变化,如图29-2所示。

image-20210930145947637

图29-2 服装公司改头换面后的类图
好,我的企业改头换面了,开始生产iPod产品了,看我IPodCorp类的实现,如代码清单29-5所示。

代码清单29-5 iPod山寨公司

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IPodCorp extends Corp {
//我开始生产iPod了
protected void produce() {
System.out.println("我生产iPod...");
}
//山寨的iPod很畅销,便宜嘛
protected void sell() {
System.out.println("iPod畅销...");
}
//狂赚钱
public void makeMoney(){
super.makeMoney();
System.out.println("我赚钱呀...");
}
}

服装工厂改成了电子工厂,你这个董事长还是要去看看到底生产什么的,场景类如代码清单29-6所示。

代码清单29-6 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
System.out.println("-------房地产公司是按这样运行的-------");
//先找到我的公司
HouseCorp houseCorp =new HouseCorp();
//看我怎么挣钱
houseCorp.makeMoney();
System.out.println("\n");
System.out.println("-------山寨公司是按这样运行的-------");
IPodCorp iPodCorp = new IPodCorp();
iPodCorp.makeMoney();
}
}

确实,只用修改了黑色字体这几句话,服装厂就开始变成山寨iPod生产车间,然后你就看着你的财富在积累。山寨的东西不需要特别的销售渠道(正品到哪里我就到哪里),不需要维修成本(大不了给你换个,你还想怎么样,过了高峰期我就改头换面了,你找谁维修去?投诉?投诉谁呢?),不承担广告成本(正品在打广告,我还需要吗?需要吗?),但是也有犯愁的时候,这是一个山寨工厂,要及时地生产出市场上流行的产品,转型要快,要灵活,今天从生产iPod转为生产MP4,明天再转为生产上网本,这都需要灵活的变化,不要限制得太死!那问题来了,每次我的厂房,我的工人,我的设备都在,不可能每次我换个山寨产品厂子就彻底不要了。这不行,成本忒高了点,那怎么办?

Thinking,Thinking…I got an idea!(跳跳虎语),既然产品和工厂绑得太死,那我就给你来 松松,改变设计,如图29-3所示。

image-20210930150200692

图29-3 使用快速变化的类图
公司和产品之间建立关联关系,可以彻底解决以后山寨公司生产产品的问题,工厂想换产品?太容易了!看程序说话,先看Product抽象类,如代码清单29-7所示。

代码清单29-7 抽象产品类

1
2
3
4
5
6
public abstract class Product {
//甭管是什么产品它总要能被生产出来
public abstract void beProducted();
//生产出来的东西,一定要销售出去,否则亏本
public abstract void beSelled();
}

简单!忒简单了!House产品类如代码清单29-8所示。

代码清单29-8 房子

1
2
3
4
5
6
7
8
9
10
public class House extends Product {
//豆腐渣就豆腐渣呗,好歹也是房子
public void beProducted() {
System.out.println("生产出的房子是这样的...");
}
//虽然是豆腐渣,也是能够销售出去的
public void beSelled() {
System.out.println("生产出的房子卖出去了...");
}
}

既然是产品类,那肯定有两种行为要存在:被生产和被销售,否则就不能称为产品了。我们再来看iPod产品类,如代码清单29-9所示。

代码清单29-9 iPod产品

1
2
3
4
5
6
7
8
public class IPod extends Product {
public void beProducted() {
System.out.println("生产出的iPod是这样的...");
}
public void beSelled() {
System.out.println("生产出的iPod卖出去了...");
}
}

产品是由公司生产出来的,我们来看公司Corp抽象类,如代码清单29-10所示。

代码清单29-10 抽象公司类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Corp {
//定义一个抽象的产品对象,不知道具体是什么产品
private Product product;
//构造函数,由子类定义传递具体的产品进来
public Corp(Product product){
this.product = product;
}
//公司是干什么的?赚钱的!
public void makeMoney(){
//每家公司都是一样,先生产
this.product.beProducted();
//然后销售
this.product.beSelled();
}
}

这里多了个有参构造,其目的是要继承的子类都必选重写自己的有参构造函数,把产品类传递进来,再看子类HouseCorp的实现,如代码清单29-11所示。

代码清单29-11 房地产公司

1
2
3
4
5
6
7
8
9
10
11
public class HouseCorp extends Corp {
//定义传递一个House产品进来
public HouseCorp(House house){
super(house);
}
//房地产公司很High了,赚钱,计算利润
public void makeMoney(){
super.makeMoney();
System.out.println("房地产公司赚大钱了...");
}
}

理解上没有多少难度,不多说,继续看山寨公司的实现,如代码清单29-12所示。

代码清单29-12 山寨公司

1
2
3
4
5
6
7
8
9
10
11
public class ShanZhaiCorp extends Corp {
//产什么产品,不知道,等被调用的才知道
public ShanZhaiCorp(Product product){
super(product);
}
//狂赚钱
public void makeMoney(){
super.makeMoney();
System.out.println("我赚钱呀...");
}
}

HouseCorp类和ShanZhaiCorp类的区别是在有参构造的参数类型上,HouseCorp类比较明 确,我就是只要House类,所以直接定义传递进来的必须是House类, 一个类尽可能少地承 担职责,那方法也一样,既然HouseCorp类已经非常明确地只生产House产品,那为什么不定 义成House类型呢?ShanZhaiCorp就不同了,它确定不了生产什么类型。

好了,两大对应的阵营都已经产生了。我们再看Client程序,如代码清单29-13所示。

代码清单29-13 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) {
House house = new House();
System.out.println("-------房地产公司是这样运行的-------");
//先找到房地产公司
HouseCorp houseCorp =new HouseCorp(house);
//看我怎么挣钱
houseCorp.makeMoney();
System.out.println("\n");
//山寨公司生产的产品很多,不过我只要指定产品就成了
System.out.println("-------山寨公司是这样运行的-------");
ShanZhaiCorp shanZhaiCorp = new ShanZhaiCorp(new IPod());
shanZhaiCorp.makeMoney();
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
-------房地产公司是这样运行的------- 
生产出的房子是这样的...
生产出的房子卖出去了...
房地产公司赚大钱了...
-------山寨公司是这样运行的-------
生产出的iPod是这个样子的...
生产出的iPod卖出去了...
我赚钱呀...

突然有一天,老板良心发现了,不准备生产这种“三无”产品了,那我们程序该怎么修改呢?如果仍重操旧业,生产衣服,那该如何处理呢?很容易处理,增加一个产品类,然后稍稍修改一下场景就可以了,我们来看衣服产品类,如代码清单29-14所示。

代码清单29-14 服装

1
2
3
4
5
6
7
8
public class Clothes extends Product {
public void beProducted() {
System.out.println("生产出的衣服是这样的...");
}
public void beSelled() {
System.out.println("生产出的衣服卖出去了...");
}
}

然后再稍稍修改一下场景类,如代码清单29-15所示。

代码清单29-15 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) {
House house = new House();
System.out.println("-------房地产公司是这样运行的-------");
//先找到房地产公司
HouseCorp houseCorp =new HouseCorp(house);
//看我怎么挣钱
houseCorp.makeMoney();
System.out.println("\n");
//山寨公司生产的产品很多,不过我只要指定产品就成了
System.out.println("-------山寨公司是这样运行的-------");
ShanZhaiCorp shanZhaiCorp = new ShanZhaiCorp(new Clothes());
shanZhaiCorp.makeMoney();
}
}

修改后的运行结果如下所示:

1
2
3
4
5
6
7
8
-------房地产公司是这样运行的------- 
生产出的房子是这样的...
生产出的房子卖出去了...
房地产公司赚大钱了...
-------山寨公司是这样运行的-------
生产出的衣服是这样的...
生产出的衣服卖出去了...
我赚钱呀...

看代码中的黑体部分,就修改了这一条语句就完成了生产产品的转换。那我们深入思考一下,既然万物都是运动的,我现在只有房地产公司和山寨公司,那以后我会不会增加一些其他的公司呢?或者房地产公司会不会对业务进行细化,如分为公寓房公司、别墅公司,以及商业房公司等呢?那我告诉你,会的!绝对会的!但是你发觉没有,这种变化对我们上面的类图来说不会做大的修改,充其量只是扩展:

  • 增加公司,要么继承Corp类,要么继承HouseCorp或ShanZhaiCorp,不用再修改原有的 类了。
  • 增加产品,继承Product类,或者继承House类,你要把房子分为公寓房、别墅、商业 用房等。

你唯一要修改的就是Client类。类都增加了,高层模块也需要修改,也就是说Corp类和Product类都可以自由地扩展,而不会对整个应用产生太大的变更,这就是桥梁模式。

29.2 桥梁模式的定义

桥梁模式(Bridge Pattern)也叫做桥接模式,是一个比较简单的模式,其定义如下:

Decouple an abstraction from its implementation so that the two can vary independently.(将抽象和实现解耦,使得两者可以独立地变化。)

桥梁模式的重点是在“解耦”上,如何让它们两者解耦是我们要了解的重点,我们先来看桥梁模式的通用类,如图29-4所示。

image-20210930150840802

图29-4 桥梁模式通用类图

我们先来看桥梁模式中的4个角色。

  • Abstraction——抽象化角色

它的主要职责是定义出该角色的行为,同时保存一个对实现化角色的引用,该角色一般是抽象类。

  • Implementor——实现化角色

它是接口或者抽象类,定义角色必需的行为和属性。

  • RefinedAbstraction——修正抽象化角色

它引用实现化角色对抽象化角色进行修正。

  • ConcreteImplementor——具体实现化角色

它实现接口或抽象类定义的方法和属性。

桥梁模式中的几个名词比较拗口,大家只要记住一句话就成:抽象角色引用实现角色, 或者说抽象角色的部分实现是由实现角色完成的。我们来看其通用源码,先看实现化角色, 如代码清单29-16所示。

代码清单29-16 实现化角色

1
2
3
4
5
public interface Implementor {
//基本方法
public void doSomething();
public void doAnything();
}

它没有任何特殊的地方,就是一个一般的接口,定义要实现的方法。其实现类如代码清单29-17所示。

代码清单29-17 具体实现化角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ConcreteImplementor1 implements Implementor{
public void doSomething(){
//业务逻辑处理
}
public void doAnything(){
//业务逻辑处理
}
}
public class ConcreteImplementor2 implements Implementor{
public void doSomething(){
//业务逻辑处理
}
public void doAnything(){
//业务逻辑处理
}
}

上面定义了两个具体实现化角色——代表两个不同的业务逻辑。我们再来看抽象化角色,如代码清单29-18所示。

代码清单29-18 抽象化角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class Abstraction {
//定义对实现化角色的引用
private Implementor imp;
//约束子类必须实现该构造函数
public Abstraction(Implementor _imp){
this.imp = _imp;
}
//自身的行为和属性
public void request(){
this.imp.doSomething();
}
//获得实现化角色
public Implementor getImp(){
return imp;
}
}

各位可能要问,为什么要增加一个构造函数?答案是为了提醒子类,你必须做这项工作,指定实现者,特别是已经明确了实现者,则尽量清晰明确地定义出来。我们来看具体的抽象化角色,如代码清单29-19所示。

代码清单29-19 具体抽象化角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RefinedAbstraction extends Abstraction {
//覆写构造函数
public RefinedAbstraction(Implementor _imp){
super(_imp);
}
//修正父类的行为
@Override
public void request(){
/*
* 业务处理...
*/
super.request();
super.getImp().doAnything();
}
}

想想看,如果我们的实现化角色有很多的子接口,然后是一堆的子实现。如果在构造函数中不传递一个尽量明确的实现者,代码就很不清晰。我们来看场景类如何模拟,如代码清单29-20所示。

代码清单29-20 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//定义一个实现化角色
Implementor imp = new ConcreteImplementor1();
//定义一个抽象化角色
Abstraction abs = new RefinedAbstraction(imp);
//执行行文
abs.request();
}
}

桥梁模式是一个非常简单的模式,它只是使用了类间的聚合关系、继承、覆写等常用功能,但是它却提供了一个非常清晰、稳定的架构。

30.1 工厂方法模式VS建造者模式

工厂方法模式注重的是整体对象的创建方法,而建造者模式注重的是部件构建的过程, 旨在通过一步一步地精确构造创建出一个复杂的对象。我们举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了。纯粹使用文字来描述比较枯燥,我们还是通过程序来更加清晰地认识两者的差别。

30.1.1 按工厂方法建造超人

首先,按照工厂方法模式创建出一个超人,类图如图30-1所示。

image-20210930154828157

图30-1 按工厂方法建造超人

类图中我们按照年龄段把超人分为两种类型:成年超人(如克拉克、超能先生)和未成年超人(如Dash、Jack)。这是一个非常正宗的工厂方法模式,定义一个产品的接口,然后再定义两个实现,通过超人制造工厂制造超人。想想看我们对超人最大印象是什么?当然是他的超能力,我们以specialTalent(特殊天赋)方法来代表,先看抽象产品类,如代码清单30-1所示。

代码清单30-1 超人接口

1
2
3
4
public interface ISuperMan {
//每个超人都有特殊技能
public void specialTalent();
}

产品的接口定义好了,我们再来看具体的产品。先看成年超人,很简单,如代码清单30-2所示。

代码清单30-2 成年超人

1
2
3
4
5
6
public class AdultSuperMan implements ISuperMan {
//超能先生
public void specialTalent() {
System.out.println("超人力大无穷");
}
}

未成年超人的代码如代码清单30-3所示。

代码清单30-3 未成年超人

1
2
3
4
5
6
public class ChildSuperMan implements ISuperMan {
//超能先生的三个孩子
public void specialTalent() {
System.out.println("小超人的能力是刀枪不入、快速运动");
}
}

产品都具备,那我们编写一个工厂类,其意图就是生产超人,具体是成年超人还是未成年超人,则由客户端决定,如代码清单30-4所示。

代码清单30-4 超人制造工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SuperManFactory {
//定义一个生产超人的工厂
public static ISuperMan createSuperMan(String type){
//根据输入参数产生不同的超人
if(type.equalsIgnoreCase("adult")){
//生产成人超人
return new AdultSuperMan();
}
else if(type.equalsIgnoreCase("child")){
//生产未成年超人
return new ChildSuperMan();
}
else{
return null;
}
}
}

产品有了,工厂类也有了,剩下的工作就是开始生产超人。这也非常简单,如代码清单30-5所示。

代码清单30-5 场景类

1
2
3
4
5
6
7
8
9
public class Client {
//模拟生产超人
public static void main(String[] args) {
//生产一个成年超人
ISuperMan adultSuperMan = SuperManFactory.createSuperMan("adult");
//展示一下超人的技能
adultSuperMan.specialTalent();
}
}

建立了一个超人生产工厂,年复一年地生产超人,对于具体生产出的产品,不管是成年超人还是未成年超人,都是一个模样:深蓝色紧身衣、胸前S标记、内裤外穿,没有特殊的地方。但是我们的目的达到了——生产出超人,拯救全人类,这就是我们的意图。具体怎么生产、怎么组装,这不是工厂方法模式要考虑的,也就是说,工厂模式关注的是一个产品整体,生产出的产品应该具有相似的功能和架构。


注意 通过工厂方法模式生产出对象,然后由客户端进行对象的其他操作,但是并不代 表所有生产出的对象都必须具有相同的状态和行为,它是由产品所决定。


30.1.2 按建造者模式建造超人

我们再来看看建造者模式是如何生产超人的,如图30-2所示。

image-20210930155136413

图30-2 按建造者模式生产超人
又是一个典型的建造者模式!哎,不对呀!通用模式上抽象建造者与产品类没有关系呀!是的,我们当然可以加强了,我们在抽象建造者上使用了模板方法模式,每一个建造者都必须返回一个产品,但是产品是如何制造的,则由各个建造者自己负责。我们来看看程序,先看产品类,如代码清单30-6所示。

代码清单30-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
public class SuperMan {
//超人的躯体
private String body;
//超人的特殊技能
private String specialTalent;
//超人的标志
private String specialSymbol;
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getSpecialTalent() {
return specialTalent;
}
public void setSpecialTalent(String specialTalent) {
this.specialTalent = specialTalent;
}
public String getSpecialSymbol() {
return specialSymbol;
}
public void setSpecialSymbol(String specialSymbol) {
this.specialSymbol = specialSymbol;
}
}

超人这个产品是由三部分组成:躯体、特殊技能、身份标记,这就类似于电子产品,首先生产出一个固件,然后再安装一个灵魂(软件驱动),最后再打上产品标签。完事了!一个崭新的产品就诞生了!我们的超人也是这样生产的,先生产一个普通的躯体,然后注入特殊技能,最后打上S标签,一个超人生产完毕。我们再来看一下建造者的抽象定义,如代码清单30-7所示。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Builder {
//定义一个超人的应用
protected final SuperMan superMan = new SuperMan();
//构建出超人的躯体
public void setBody(String body){
this.superMan.setBody(body);
}
//构建出超人的特殊技能
public void setSpecialTalent(String st){
this.superMan.setSpecialTalent(st);
}
//构建出超人的特殊标记
public void setSpecialSymbol(String ss){
this.superMan.setSpecialSymbol(ss);
}
//构建出一个完整的超人
public abstract SuperMan getSuperMan();
}

一个典型的模板方法模式,超人的各个部件(躯体、灵魂、标志)都准备好了,具体怎么组装则是由实现类来决定。我们先来看成年超人,如代码清单30-8所示。

代码清单30-8 成年超人建造者

1
2
3
4
5
6
7
8
9
public class AdultSuperManBuilder extends Builder {
@Override
public SuperMan getSuperMan() {
super.setBody("强壮的躯体");
super.setSpecialTalent("会飞行");
super.setSpecialSymbol("胸前带S标记");
return super.superMan;
}
}

怎么回事?在第11章中讲解建造者模式的时候在产品中使用了模板方法模式,在这里怎么把模板方法模式迁移到建造者了?怎么会这样?你是不是在发出这样的疑问?别疑问了! 设计模式只是提供了一个解决问题的意图:复杂对象的构建与它的表示分离,而没有具体定出一个设计模式必须是这样的实现,必须是这样的代码,灵活运用模式才是其根本,别学死板了。

我们继续看未成年超人的建造者,如代码清单30-9所示。

代码清单30-9 未成年超人建造者

1
2
3
4
5
6
7
8
9
public class ChildSuperManBuilder extends Builder {
@Override
public SuperMan getSuperMan() {
super.setBody("强壮的躯体");
super.setSpecialTalent("刀枪不入");
super.setSpecialSymbol("胸前带小S标记");
return super.superMan;
}
}

大家注意看我们这两个具体的建造者,它们都关注了产品的各个部分,在某些应用场景下甚至会关心产品的构建顺序,即使是相同的部件,装配顺序不同,产生的结果也不同,这也正是建造者模式的意图:通过不同的部件、不同装配产生不同的复杂对象。我们再来看导演类,如代码清单30-10所示。

代码清单30-10 导演类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Director {
//两个建造者的应用
private static Builder adultBuilder = new AdultSuperManBuilder();
//未成年超人的建造者
private static Builder childBuilder = new ChildSuperManBuilder();
//建造一个成年、会飞行的超人
public static SuperMan getAdultSuperMan(){
return adultBuilder.getSuperMan();
}
//建造一个未成年、刀枪不入的超人
public static SuperMan getChildSuperMan(){
return childBuilder.getSuperMan();
}
}

这很简单,不多说了!看看场景类是如何调用的,如代码清单30-11所示。

代码清单30-11 场景类

1
2
3
4
5
6
7
8
public class Client {
public static void main(String[] args) {
//建造一个成年超人
SuperMan adultSuperMan = Director.getAdultSuperMan();
//展示一下超人的信息
adultSuperMan.getSpecialTalent();
}
}

这个场景类的写法与工厂方法模式是相同的,但是你可以看到,在建立超人的过程中, 建造者必须关注超人的各个部件,而工厂方法模式则只关注超人的整体,这就是两者的区别。

30.1.3 最佳实践

工厂方法模式和建造者模式都属于对象创建类模式,都用来创建类的对象。但它们之间的区别还是比较明显的。

  • 意图不同

在工厂方法模式里,我们关注的是一个产品整体,如超人整体,无须关心产品的各部分是如何创建出来的;但在建造者模式中,一个具体产品的产生是依赖各个部件的产生以及装配顺序,它关注的是“由零件一步一步地组装出产品对象”。简单地说,工厂模式是一个对象创建的粗线条应用,建造者模式则是通过细线条勾勒出一个复杂对象,关注的是产品组成部分的创建过程。

  • 产品的复杂度不同

工厂方法模式创建的产品一般都是单一性质产品,如成年超人,都是一个模样,而建造者模式创建的则是一个复合产品,它由各个部件复合而成,部件不同产品对象当然不同。这不是说工厂方法模式创建的对象简单,而是指它们的粒度大小不同。一般来说,工厂方法模式的对象粒度比较粗,建造者模式的产品对象粒度比较细。

两者的区别有了,那在具体的应用中,我们该如何选择呢?是用工厂方法模式来创建对象,还是用建造者模式来创建对象,这完全取决于我们在做系统设计时的意图,如果需要详细关注一个产品部件的生产、安装步骤,则选择建造者,否则选择工厂方法模式。

31.1 代理模式VS装饰模式

对于两个模式,首先要说的是,装饰模式就是代理模式的一个特殊应用,两者的共同点是都具有相同的接口,不同点则是代理模式着重对代理过程的控制,而装饰模式则是对类的功能进行加强或减弱,它着重类的功能变化,我们举例来说明它们的区别。

31.1.1 代理模式

一个著名的短跑运动员有自己的代理人。如果你很仰慕他,你找运动员说“你跑个我看看”,运动员肯定不搭理你,不过你找到他的代理人就不一样了,你可能和代理人比较熟, 可以称兄道弟,这个忙代理人还是可以帮的,于是代理人同意让你欣赏运动员的练习赛,这对你来说已经是莫大的荣耀了。我们来看类图,如图31-1所示。

image-20210930161729112

图31-1 运动员跑步
这是一个套用代理模式的简单应用,非常简单!一个对象,然后再是自己的代理。我们先来看一下代码,先看抽象主题类,如代码清单31-1所示。

代码清单31-1 抽象运动员

1
2
3
4
public interface IRunner {
//运动员的主要工作就是跑步
public void run();
}

一个具体的短跑运动员跑步是很潇洒的,如代码清单31-2所示。

代码清单31-2 运动员跑步

1
2
3
4
5
public class Runner implements IRunner {
public void run() {
System.out.println("运动员跑步:动作很潇洒");
}
}

看看现在的明星运动员,一般都有自己的代理人,要么是专职的,要么就是自己的教练兼职,那我们来看看代理人的职责,如代码清单31-3所示。

代码清单31-3 代理人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RunnerAgent implements IRunner {
private IRunner runner;
public RunnerAgent(IRunner _runner){
this.runner = _runner;
}
//代理人是不会跑的
public void run() {
Random rand = new Random();
if(rand.nextBoolean()){
System.out.println("代理人同意安排运动员跑步");
runner.run();
}
else{
System.out.println("代理人心情不好,不安排运动员跑步");
}
}
}

我们只是定义了一个代理人,并没有明确定义是哪一个运动员的代理,需要在运行时指定被代理者,而且我们还在代理人的run方法中做了判断,想让被代理人跑步就跑步,不乐意就拒绝,对于主题类的行为是否可以发生,代理类有绝对的控制权。我们编写一个场景类来模拟这种情况,如代码清单31-4所示。

代码清单31-4 场景类

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) {
//定义一个短跑运动员
IRunner liu = new Runner();
//定义liu的代理人
IRunner agent = new RunnerAgent(liu);
//要求运动员跑步
System.out.println("====客人找到运动员的代理要求其去跑步===");
agent.run();
}
}

由于我们使用了随机数产生模拟结果,因此运行结果有两种可能情况,第一种情况如下所示:

1
2
3
4
5
6
====客人找到运动员的代理要求其去跑步=== 
代理人同意安排运动员跑步
运动员跑步:动作很潇洒
运行结果的第二种情况如下所示:
====客人找到运动员的代理要求其去跑步===
代理人心情不好,不安排运动员跑步

不管是哪种情况,我们都证实了代理的一个功能:在不改变接口的前提下,对过程进行控制。在我们例子中,运动员要不要跑步是由代理人决定的,代理人说跑步就跑步,说不跑就不跑,它有绝对判断权。

31.1.2 装饰模式

如果使用装饰模式,我们该怎么实现这个过程呢?装饰模式是对类功能的加强,怎么加强呢?增强跑步速度!在屁股后面安装一个喷气动力装置,类似火箭的喷气装置,那速度变得很快,《蜘蛛侠》中的那个反面角色不就是这样的吗?好,我们来看类图,如图31-2所示。

image-20210930162043387

图31-2 增强运动员的功能
很惊讶?这个代理模式完全一样的类图?是的,完全一样!不过其实现的意图却不同, 我们先来看代码,IRunner和Runner与代理模式相同,详见代码清单31-1和代码清单31-2所示,在此不再赘述。我们来看装饰类RunnerWithJet,如代码清单31-5所示。

代码清单31-5 装饰类

1
2
3
4
5
6
7
8
9
10
public class RunnerWithJet implements IRunner {
private IRunner runner;
public RunnerWithJet(IRunner _runner){
this.runner = _runner;
}
public void run() {
System.out.println("加快运动员的速度:为运动员增加喷气装置");
runner.run();
}
}

这和代理模式中的代理类也是非常相似的,只是装饰类对类的行为没有决定权,只有增强作用,也就是说它不决定被代理的方法是否执行,它只是再次增加被代理的功能。我们来看场景类,如代码清单31-6所示。

代码清单31-6 场景类

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) {
//定义运动员
IRunner liu = new Runner();
//对其功能加强
liu = new RunnerWithJet(liu);
//看看它的跑步情况如何
System.out.println("===增强后的运动员的功能===");
liu.run();
}
}

运行结果如下所示:

1
2
3
===增强后的运动员的功能=== 
加快运动员的速度:为运动员增加喷气装置
运动员跑步:动作很潇洒

注意思考一下我们的程序,我们通过增加了一个装饰类,就完成了对原有类的功能增加,由一个普通的短跑运动员变成了带有喷气装置的超人运动员,其速度岂是普通人能相比的?!

31.1.3 最佳实践

31.2 装饰模式VS适配器模式

装饰模式和适配器模式在通用类图上没有太多的相似点,差别比较大,但是它们的功能有相似的地方:都是包装作用,都是通过委托方式实现其功能。不同点是:装饰模式包装的是自己的兄弟类,隶属于同一个家族(相同接口或父类),适配器模式则修饰非血缘关系类,把一个非本家族的对象伪装成本家族的对象,注意是伪装,因此它的本质还是非相同接口的对象。

大家都应该听过丑小鸭的故事吧,我们今天就用这两种模式分别讲述丑小鸭的故事。话说鸭妈妈有5个孩子,其中4个孩子都是黄白相间的颜色,而最小的那只也就是叫做丑小鸭的那只,是纯白色的,与兄弟姐妹不相同,在遭受了诸多的嘲讽和讥笑后,最终丑小鸭变成了一只美丽的天鹅。那我们如何用两种不同模式来描述这一故事呢?

31.2.1 用装饰模式描述丑小鸭

用装饰模式来描述丑小鸭,首先就要肯定丑小鸭是一只天鹅,只是因为她小或者是鸭妈妈的无知才没有被认出是天鹅,经过一段时间后,它逐步变成一个漂亮、自信、优美的白天鹅。根据分析我们可以这样设计,先设计一个丑小鸭,然后根据时间先后来进行不同的美化处理,怎么美化呢?先长出漂亮的羽毛,然后逐步展现出异于鸭子的不同行为,如飞行,最终在具备了所有的行为后,它就成为一只纯粹的白天鹅了,我们来看类图,如图31-3所示。

image-20210930162553626

图31-3 装饰模式实现丑小鸭

类图比较简单,非常标准的装饰模式。我们按照故事的情节发展一步一步地实现程序。 初期的时候,丑小鸭表现得很另类,叫声不同,外形不同,致使周围的亲戚、朋友都对她鄙视,那我们来建立这个过程,由于丑小鸭的本质就是一个天鹅,我们就先生成一个天鹅的接口,如代码清单31-7所示。

代码清单31-7 天鹅接口

1
2
3
4
5
6
7
8
public interface Swan {
//天鹅会飞
public void fly();
//天鹅会叫
public void cry();
//天鹅都有漂亮的外表
public void desAppaearance();
}

我们定义了天鹅的行为,都会飞行、会叫,并且可以描述她们漂亮的外表。丑小鸭是一只白天鹅,是”is-a”的关系,也就是需要实现这个接口了,其实现如代码清单31-8所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UglyDuckling implements Swan {
//丑小鸭的叫声
public void cry() {
System.out.println("叫声是克噜——克噜——克噜");
}
//丑小鸭的外形
public void desAppaearance() {
System.out.println("外形是脏兮兮的白色,毛茸茸的大脑袋");
}
//丑小鸭还比较小,不能飞
public void fly() {
System.out.println("不能飞行");
}
}

丑小鸭具备了天鹅的所有行为和属性,因为她本来就是一只白天鹅,只是因为她太小了还不能飞行,也不能照顾自己,所以丑丑的,在经过长时间的流浪生活后,丑小鸭长大了。 终于有一天,她发现自己竟然变成了一只美丽的白天鹅,有着漂亮、洁白的羽毛,而且还可以飞行,这完全是一种升华行为。我们来看看她的行为(飞行)和属性(外形)是如何加强的,先看抽象的装饰类,如代码清单31-9所示。

代码清单31-9 抽象装饰类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Decorator implements Swan {
private Swan swan;
//修饰的是谁
public Decorator(Swan _swan){
this.swan =_swan;
}
public void cry() {
swan.cry();
}
public void desAppaearance() {
swan.desAppaearance();
}
public void fly() {
swan.fly();
}
}

这是一个非常简单的代理模式。我们再来看丑小鸭是如何开始变得美丽的,变化是由外及里的,有了漂亮的外表才有内心的实质变化,如代码清单31-10所示。

代码清单31-10 外形美化

1
2
3
4
5
6
7
8
9
10
11
public class BeautifyAppearance extends Decorator {
//要美化谁
public BeautifyAppearance(Swan _swan){
super(_swan);
}
//外表美化处理
@Override
public void desAppaearance(){
System.out.println("外表是纯白色的,非常惹人喜爱!");
}
}

丑小鸭最后发现自己还能飞行,这是一个行为突破,是对原有行为“不会飞行”的一种强化,如代码清单31-11所示。

代码清单31-11 强化行为

1
2
3
4
5
6
7
8
9
10
public class StrongBehavior extends Decorator {
//强化谁
public StrongBehavior(Swan _swan){
super(_swan);
}
//会飞行了
public void fly(){
System.out.println("会飞行了!");
}
}

所有的故事元素我们都具备了,就等有人来讲故事了,场景类如代码清单31-12所示。

代码清单31-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
public class Client {
public static void main(String[] args) {
//很久很久以前,这里有一个丑陋的小鸭子
System.out.println("===很久很久以前,这里有一只丑陋的小鸭子===");
Swan duckling = new UglyDuckling();
//展示一下小鸭子
duckling.desAppaearance();
//小鸭子的外形
duckling.cry();
//小鸭子的叫声
duckling.fly();
//小鸭子的行为
System.out.println("\n===小鸭子终于发现自己是一只天鹅====");
//首先外形变化了
duckling = new BeautifyAppearance(duckling);
//其次行为也发生了改变
duckling = new StrongBehavior(duckling);
//虽然还是叫丑小鸭,但是已经发生了很大变化
duckling.desAppaearance();
//小鸭子的新外形
duckling.cry();
//小鸭子的新叫声
duckling.fly();
//小鸭子的新行为
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
===很久很久以前,这里有一只丑陋的小鸭子=== 
外形是脏兮兮的白色,毛茸茸的大脑袋
叫声是克噜——克噜——克噜
不能飞行
===小鸭子终于发现自己是一只天鹅====
外表是纯白色的,非常惹人喜爱!
叫声是克噜——克噜——克噜
会飞行了!

使用装饰模式描述丑小鸭蜕变的过程是如此简单,它关注了对象功能的强化,是对原始对象的行为和属性的修正和加强,把原本被人歧视、冷落的丑小鸭通过两次强化处理最终转变为受人喜爱、羡慕的白天鹅。

31.2.2 用适配器模式实现丑小鸭

采用适配器模式实现丑小鸭变成白天鹅的过程要从鸭妈妈的角度来分析,鸭妈妈有5个孩子,它认为这5个孩子都是她的后代,都是鸭类,但是实际上是有一只(也就是丑小鸭) 不是真正的鸭类,她是一只小白天鹅,就像《木兰辞》中说的“雄兔脚扑朔,雌兔眼迷离。 双兔傍地走,安能辨我是雄雌?”同样,因为太小,差别太细微,很难分辨,导致鸭妈妈认为她是一只鸭子,从鸭子的审美观来看,丑小鸭是丑陋的。通过分析,我们要做的就是要设计两个对象:鸭和天鹅,然后鸭妈妈把一只天鹅看成了小鸭子,最终时间到来的时候丑小鸭变成了白天鹅。我们来看类图,如图31-4所示。

image-20210930163243263

图31-4 适配器模式实现丑小鸭

类图非常简单,我们定义了两个接口:鸭类接口和天鹅类接口,然后建立了一个适配器UglyDuckling,把一只白天鹅封装成了小鸭子。我们来看代码,先看鸭类接口,如代码清单31-13所示。

代码清单31-13 鸭类接口

1
2
3
4
5
6
7
8
public interface Duck {
//会叫
public void cry();
//鸭子的外形
public void desAppearance();
//描述鸭子的其他行为
public void desBehavior();
}

鸭类有3个行为,一个是鸭会叫,一个是外形描述,还有一个是综合性的其他行为描述,例如会游泳等。我们来看鸭妈妈的4个正宗孩子,如代码清单31-14所示。

代码清单31-14 小鸭子

1
2
3
4
5
6
7
8
9
10
11
12
public class Duckling implements Duck {
public void cry() {
System.out.println("叫声是嘎——嘎——嘎");
}
public void desAppearance() {
System.out.println("外形是黄白相间,嘴长");
}
//鸭子的其他行为,如游泳
public void desBehavior(){
System.out.println("会游泳");
}
}

4只正宗的小鸭子形象已经清晰地定义出来了。鸭妈妈还有一个孩子,就是另类的丑小 鸭,她实际是一只白天鹅。我们先定义出白天鹅,如代码清单31-15所示。

代码清单31-15 白天鹅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class WhiteSwan implements Swan {
//白天鹅的叫声
public void cry() {
System.out.println("叫声是克噜——克噜——克噜");
}
//白天鹅的外形
public void desAppaearance() {
System.out.println("外形是纯白色,惹人喜爱");
}
//天鹅是能够飞行的
public void fly() {
System.out.println("能够飞行");
}
}

但是,鸭妈妈却不认为自己这个另类的孩子是白天鹅,它从自己的观点出发,认为她很丑陋,有碍自己的脸面,于是驱赶她——鸭妈妈把这只小天鹅误认为一只鸭。我们来看实现,如代码清单31-16所示。

代码清单31-16 把白天鹅当做小鸭子看待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UglyDuckling extends WhiteSwan implements Duck {
//丑小鸭的叫声
public void cry() {
super.cry();
}
//丑小鸭的外形
public void desAppearance() {
super.desAppaearance();
}
//丑小鸭的其他行为
public void desBehavior(){
//丑小鸭不仅会游泳
System.out.println("会游泳");
//还会飞行
super.fly();
}
}

天鹅被看成了鸭子,有点暴殄天物的感觉。我们再来创建一个场景类来描述这一场景, 如代码清单31-17所示。

代码清单31-17 场景类

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) {
//鸭妈妈有5个孩子,其中4个都是一个模样
System.out.println("===妈妈有五个孩子,其中四个模样是这样的:===");
Duck duck = new Duckling();
duck.cry();
//小鸭子的叫声
duck.desAppearance();
//小鸭子的外形
duck.desBehavior();
//小鸭子的其他行为
System.out.println("\n===一只独特的小鸭子,模样是这样的:===");
Duck uglyDuckling = new UglyDuckling();
uglyDuckling.cry();
//丑小鸭的叫声
uglyDuckling.desAppearance();
//丑小鸭的外形
uglyDuckling.desBehavior();
//丑小鸭的其他行为
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
9
===妈妈有5个孩子,其中4个模样是这样的:=== 
叫声是嘎——嘎——嘎
外形是黄白相间,嘴长
会游泳
===一只独特的小鸭子,模样是这样的:===
叫声是克噜——克噜——克噜
外形是纯白色,惹人喜爱
会游泳
能够飞行

可怜的小天鹅被认为是一只丑陋的小鸭子,造物弄人呀!采用适配器模式讲述丑小鸭的故事,我们首先观察到的是鸭与天鹅的不同点,建立了不同的接口以实现不同的物种,然后在需要的时候(根据故事情节)把一个物种伪装成另外一个物种,实现不同物种的相同处理过程,这就是适配器模式的设计意图。

31.2.3 最佳实践

我们用两个模式实现了丑小鸭的美丽蜕变。我们发现:这两个模式有较多的不同点。

  • 意图不同

装饰模式的意图是加强对象的功能,例子中就是把一个怯弱的小天鹅强化成了一个美丽、自信的白天鹅,它不改变类的行为和属性,只是增加(当然了,减弱类的功能也是可能存在的)功能,使美丽更加美丽,强壮更加强壮,安全更加安全;而适配器模式关注的则是转化,它的主要意图是两个不同对象之间的转化,它可以把一个天鹅转化为一个小鸭子看待,也可以把一只小鸭子看成是一只天鹅(那估计要在小鸭子的背上装个螺旋桨了),它关注转换。

  • 施与对象不同

装饰模式装饰的对象必须是自己的同宗,也就是相同的接口或父类,只要在具有相同的属性和行为的情况下,才能比较行为是增加还是减弱;适配器模式则必须是两个不同的对象,因为它着重于转换,只有两个不同的对象才有转换的必要,如果是相同对象还转换什么?!

  • 场景不同

装饰模式在任何时候都可以使用,只要是想增强类的功能,而适配器模式则是一个补救模式,一般出现在系统成熟或已经构建完毕的项目中,作为一个紧急处理手段采用。

  • 扩展性不同

装饰模式很容易扩展!今天不用这个修饰,好,去掉;明天想再使用,好,加上。这都没有问题。而且装饰类可以继续扩展下去;但是适配器模式就不同了,它在两个不同对象之间架起了一座沟通的桥梁,建立容易,去掉就比较困难了,需要从系统整体考虑是否能够撤销。

32.2 策略模式VS状态模式

在行为类设计模式中,状态模式和策略模式是亲兄弟,两者非常相似,我们先看看两者的通用类图,把两者放在一起比较一下,如图32-3所示。

image-20210930171106992

图32-3 策略模式(左)和状态模式(右)的通用类图

两个类图非常相似,都是通过Context类封装一个具体的行为,都提供了一个封装的方法,是高扩展性的设计模式。但根据两者的定义,我们发现两者的区别还是很明显的:策略模式封装的是不同的算法,算法之间没有交互,以达到算法可以自由切换的目的;而状态模式封装的是不同的状态,以达到状态切换行为随之发生改变的目的。这两种模式虽然都有变换的行为,但是两者的目标却是不同的。我们举例来说明两者的不同点。

人只要生下来就有工作可做,人在孩童时期的主要工作就是玩耍(学习只是在人类具有了精神意识行为后才产生的);成人时期的主要工作是养活自己,然后为社会做贡献;老年时期的主要工作就是享受天伦之乐。按照策略模式来分析,这三种不同的工作方式就是三个不同的具体算法,随着时光的推移工作内容随之更替,这和对一堆数组的冒泡排序、快速排序、插入排序一样,都是一系列的算法;而按照状态模式进行设计,则认为人的状态(孩童、成人、老人)产生了不同的行为结果,这里的行为都相同,都是工作,但是它们的实现方式确实不同,也就是产生的结果不同,看起来就像是类改变了。

32.2.1 策略模式实现人生

下面按照策略模式进行设计,先来看类图,如图32-4所示。

这是非常典型的策略模式,没有太多的玄机,它定义了一个工作算法,然后有三个实现类:孩童工作、成年人工作和老年人工作。我们来看代码,首先看抽象工作算法,如代码清单32-19所示。

image-20210930171228691

图32-4 策略模式实现人生的类图

代码清单32-19 抽象工作算法

1
2
3
4
public abstract class WorkAlgorithm {
//每个年龄段都必须完成的工作
public abstract void work();
}

无论如何,每个算法都必须实现work方法,完成对工作内容的定义,三个具体的工作算法如代码清单32-20、32-21、32-22所示。

代码清单32-20 孩童工作

1
2
3
4
5
6
7
public class ChildWork extends WorkAlgorithm {
//小孩的工作
@Override
public void work() {
System.out.println("儿童的工作是玩耍!");
}
}

代码清单32-21 成年人工作

1
2
3
4
5
6
7
public class AdultWork extends WorkAlgorithm {
//成年人的工作
@Override
public void work() {
System.out.println("成年人的工作就是先养活自己,然后为社会做贡献!");
}
}

代码清单32-22 老年人工作

1
2
3
4
5
6
7
public class OldWork extends WorkAlgorithm {
//老年人的工作
@Override
public void work() {
System.out.println("老年人的工作就是享受天伦之乐!");
}
}

我们再来看环境角色,如代码清单32-23所示。

代码清单32-23 环境角色

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Context {
private WorkAlgorithm workMethod;
public WorkAlgorithm getWork() {
return workMethod;
}
public void setWork(WorkAlgorithm work) {
this.workMethod = work;
}
//每个算法都有必须具有的功能
public void work(){
workMethod.work();
}
}

我们编写一个场景类来模拟该场景,如代码清单32-24所示。

代码清单32-24 场景类

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) {
//定义一个环境角色
Context context=new Context();
System.out.println("====儿童的主要工作=====");
context.setWork(new ChildWork());
context.work();

System.out.println("\n====成年人的主要工作=====");
context.setWork(new AdultWork());
context.work();
System.out.println("\n====老年人的主要工作=====");
context.setWork(new OldWork());
context.work();
}
}

在这里我们把每个不同的工作内容作为不同的算法,分别是孩童工作、成年人工作、老年人工作算法,然后在场景类中根据不同的年龄段匹配不同的工作内容,其运行结果如下所示:

1
2
3
4
5
6
====儿童的主要工作===== 
儿童的工作是玩耍!
====成年人的主要工作=====
成年人的工作就是先养活自己,然后为社会做贡献!
====老年人的主要工作=====
老年人的工作就是享受天伦之乐!

通过采用策略模式我们实现了“工作”这个策略的三种不同算法,算法可以自由切换,到底用哪个算法由调用者(高层模块)决定。策略模式的使用重点是算法的自由切换——老的算法退休,新的算法上台,对模块的整体功能没有非常大的改变,非常灵活。而如果想要增加一个新的算法,比如未出生婴儿的工作,只要继承WorkAlgorithm就可以了。

32.2.2 状态模式实现人生

我们再来看看使用状态模式是如何实现该需求的。随着时间的变化,人的状态变化了, 同时引起了人的工作行为改变,完全符合状态模式。我们来看类图,如图32-5所示。

image-20210930171554473

图32-5 状态模式实现人生的类图
这与策略模式非常相似,基本上就是几个类名称的修改而已,但是其中蕴藏的玄机就大了,看看代码你就会明白。我们先来看抽象状态类,如代码清单32-25所示。

代码清单32-25 人的抽象状态

1
2
3
4
5
6
7
8
9
10
public abstract class HumanState {
//指向一个具体的人
protected Human human;
//设置一个具体的人
public void setHuman(Human _human){
this.human = _human;
}
//不管人是什么状态都要工作
public abstract void work();
}

抽象状态定义了一个具体的人(human)必须进行工作(work),但是一个人在哪些状态下完成哪些工作则是由子类来实现的。我们先来看孩童状态,如代码清单32-26所示。

代码清单32-26 孩童状态

1
2
3
4
5
6
7
public class ChildState extends HumanState{
//儿童的工作就是玩耍
public void work(){
System.out.println("儿童的工作是玩耍!");
super.human.setState(Human.ADULT_STATE);
}
}

ChildState类代表孩童状态,在该状态下的工作就是玩耍。读者看着可能有点惊奇,在 work方法中为什么要设置下一个状态?因为我们的状态变化都是单方向的,从孩童到成年 人,然后到老年人,每个状态转换到其他状态只有一个方向,因此会在这里看到work有两个 职责:完成工作逻辑和定义下一状态。

我们再来看成年人状态和老年人状态,分别如代码清单32-27、32-28所示。

代码清单32-27 成年人状态

1
2
3
4
5
6
7
8
public class AdultState extends HumanState {
//成年人的工作就是先养活自己,然后为社会做贡献
@Override
public void work() {
System.out.println("成年人的工作就是先养活自己,然后为社会做贡献!");
super.human.setState(Human.OLD_STATE);
}
}

代码清单32-28 老年人状态

1
2
3
4
5
6
7
public class OldState extends HumanState {
//老年人的工作就是享受天伦之乐
@Override
public void work() {
System.out.println("老年人的工作就是享受天伦之乐!");
}
}

每一个HumanState的子类都代表了一种状态,虽然实现的方法名work都相同,但是实现的内容却不同,也就是在不同的状态下行为随之改变。我们来看环境角色是如何处理行为随状态的改变而改变的,如代码清单32-29所示。

代码清单32-29 环境角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Human {
//定义人类都具备哪些状态
public static final HumanState CHIILD_STATE = new ChildState();
public static final HumanState ADULT_STATE = new AdultState();
public static final HumanState OLD_STATE = new OldState();
//定义一个人的状态
private HumanState state;
//设置一个状态
public void setState(HumanState _state){
this.state = _state;
this.state.setHuman(this);
}
//人类的工作
public void work(){
this.state.work();
}
}

定义一个Human类代表人类,也就是状态模式中的环境角色,每个人都会经历从孩童到成年人再到老年人这样一个状态过渡(当然了,老顽童周伯通的情况我们就没有考虑进来),随着状态的改变,行为也改变。我们来看场景类,如代码清单32-30所示。

代码清单32-30 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Client {
public static void main(String[] args) {
//定义一个普通的人
Human human = new Human();
//设置一个人的初始状态
human.setState(new ChildState());
System.out.println("====儿童的主要工作=====");
human.work();
System.out.println("\n====成年人的主要工作=====");
human.work();
System.out.println("\n====老年人的主要工作=====");
human.work();
}
}

运行结果如下所示:

1
2
3
4
5
6
====儿童的主要工作===== 
儿童的工作是玩耍!
====成年人的主要工作=====
成年人的工作就是先养活自己,然后为社会做贡献!
====老年人的主要工作=====
老年人的工作就是享受天伦之乐!

运行结果与策略模式相同,但是两者的分析角度是大相径庭的。策略模式的实现是通过分析每个人的工作方式的不同而得出三个不同的算法逻辑,状态模式则是从人的生长规律来分析,每个状态对应了不同的行为,状态改变后行为也随之改变。从以上示例中我们也可以看出,对于相同的业务需求,有很多种实现方法,问题的重点是业务关注的是什么,是人的生长规律还是工作逻辑?找准了业务的焦点,才能选择一个好的设计模式。

32.2.3 小结

从例子中我们可以看出策略模式和状态模式确实非常相似,称之为亲兄弟亦不为过,但是这两者还是存在着非常大的差别,而且也是很容易区分的。

  • 环境角色的职责不同

两者都有一个叫做Context环境角色的类,但是两者的区别很大,策略模式的环境角色只是一个委托作用,负责算法的替换;而状态模式的环境角色不仅仅是委托行为,它还具有登记状态变化的功能,与具体的状态类协作,共同完成状态切换行为随之切换的任务。

  • 解决问题的重点不同

策略模式旨在解决内部算法如何改变的问题,也就是将内部算法的改变对外界的影响降低到最小,它保证的是算法可以自由地切换;而状态模式旨在解决内在状态的改变而引起行为改变的问题,它的出发点是事物的状态,封装状态而暴露行为,一个对象的状态改变,从外界来看就好像是行为改变。

  • 解决问题的方法不同

策略模式只是确保算法可以自由切换,但是什么时候用什么算法它决定不了;而状态模式对外暴露的是行为,状态的变化一般是由环境角色和具体状态共同完成的,也就是说状态模式封装了状态的变化而暴露了不同的行为或行为结果。

  • 应用场景不同

两者都能实现前面例子中的场景,但并不表示两者的应用场景相同,这只是为了更好地展示出两者的不同而设计的一个场景。我们来想一下策略模式和状态模式的使用场景有什么不同,策略模式只是一个算法的封装,可以是一个有意义的对象,也可以是一个无意义的逻辑片段,比如MD5加密算法,它是一个有意义的对象吗?不是,它只是我们数学上的一个公式的相关实现,它是一个算法,同时DES算法、RSA算法等都是具体的算法,也就是说它们都是一个抽象算法的具体实现类,从这点来看策略模式是一系列平行的、可相互替换的算法封装后的结果,这就限定了它的应用场景:算法必须是平行的,否则策略模式就封装了一堆垃圾,产生了“坏味道”。状态模式则要求有一系列状态发生变化的场景,它要求的是有状态且有行为的场景,也就是一个对象必须具有二维(状态和行为)描述才能采用状态模式,如果只有状态而没有行为,则状态的变化就失去了意义。

  • 复杂度不同

通常策略模式比较简单,这里的简单指的是结构简单,扩展比较容易,而且代码也容易阅读。当然,一个具体的算法也可以写得很复杂,只有具备很高深的数学、物理等知识的人才可以看懂,这也是允许的,我们只是说从设计模式的角度来分析,它是很容易被看懂的。 而状态模式则通常比较复杂,因为它要从两个角色看到一个对象状态和行为的改变,也就是说它封装的是变化,要知道变化是无穷尽的,因此相对来说状态模式通常都比较复杂,涉及面很多,虽然也很容易扩展,但是一般不会进行大规模的扩张和修正。

33.1 策略模式VS桥梁模式

这对冤家终于碰头了,策略模式与桥梁模式是如此相似,简直就是孪生兄弟,要把它们两个分开可不太容易。我们来看看两者的通用类图,如图33-1所示。

image-20210930184916250

图33-1 策略模式(左)和桥梁模式(右)通用类图
两者之间确实很相似。如果把策略模式的环境角色变更为一个抽象类加一个实现类,或者桥梁模式的抽象角色未实现,只有修正抽象化角色,想想看,这两个类图有什么地方不一样?完全一样!正是由于类似场景的存在才导致了两者在实际应用中经常混淆的情况发生, 我们来举例说明两者有何差别。

大家都知道邮件有两种格式:文本邮件(Text Mail)和超文本邮件(HTML MaiL),在文本邮件中只能有简单的文字信息,而在超文本邮件中可以有复杂文字(带有颜色、字体等属性)、图片、视频等,如果你使用Foxmail邮件客户端的话就应该有深刻体验,看到一份邮件,怎么没内容?原来是你忘记点击那个“HTML邮件”标签了。下面我们就来讲解如何发送这两种不同格式的邮件,研究一下这两种模式如何处理这样的场景。

33.1.1 策略模式实现邮件发送

使用策略模式发送邮件,我们认为这两种邮件是两种不同的封装格式,给定了发件人、 收件人、标题、内容的一封邮件,按照两种不同的格式分别进行封装,然后发送之。按照这样的分析,我们发现邮件的两种不同封装格式就是两种不同的算法,具体到策略模式就是两种不同策略,这样看已经很简单了,我们可以直接套用策略模式来实现。先看类图,如图33-2所示。

image-20210930200945323

图33-2 策略模式实现邮件发送的类图

我们定义了一个邮件模板,它有两个实现类:TextMail(文本邮件)和HtmlMail(超文本邮件),分别实现两种不同格式的邮件封装。MailServer是一个环境角色,它接收一个MailTemplate对象,然后通过sendMail方法发送出去。我们来看具体的代码,先看抽象邮件,如代码清单33-1所示。

代码清单33-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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public abstract class MailTemplate {
//邮件发件人
private String from;
//收件人
private String to;
//邮件标题
private String subject;
//邮件内容
private String context;
//通过构造函数传递邮件信息
public MailTemplate(String _from,String _to,String _subject,String _context){
this.from = _from;
this.to = _to;
this.subject = _subject;
this.context = _context;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public void setContext(String context){
this.context = context;
}
//邮件都有内容
public String getContext(){
return context;
}
}

很奇怪,是吗?抽象类没有抽象的方法,设置为抽象类还有什么意义呢?有意义,在这里我们定义了一个这样的抽象类:它具有邮件的所有属性,但不是一个具体可以被实例化的对象。例如,你对邮件服务器说“给我制造一封邮件”,邮件服务器肯定拒绝,为什么?你要产生什么邮件?什么格式的?邮件对邮件服务器来说是一个抽象表示,是一个可描述但不可形象化的事物。你可以这样说:“我要一封标题为XX,发件人是XXX的文本格式的邮件”, 这就是一个可实例化的对象,因此我们的设计就产生了两个子类以具体化邮件,而且每种邮件格式对邮件的内容都有不同的处理。我们首先看文本邮件,如代码清单33-2所示。

代码清单33-2 文本邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TextMail extends MailTemplate {
public TextMail(String _from, String _to, String _subject, String _context) {
super(_from, _to, _subject, _context);
}
public String getContext() {
//文本类型设置邮件的格式为:text/plain
String context = "\nContent-Type: text/plain;
charset=GB2312\n" +super.getContext();
//同时对邮件进行base64编码处理,这里用一句话代替
context = context + "\n邮件格式为:文本格式";
return context;
}
}

我们覆写了getContext方法,因为要把一封邮件设置为文本邮件必须加上一个特殊的标志:text/plain,用于告诉解析这份邮件的客户端:“我是一封文本格式的邮件,别解析错了”。同样,超文本格式的邮件也有类似的设置,如代码清单33-3所示。

代码清单33-3 超文本邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HtmlMail extends MailTemplate {
public HtmlMail(String _from, String _to, String _subject, String _context) {
super(_from, _to, _subject, _context);
}
public String getContext(){
//超文本类型设置邮件的格式为:multipart/mixed
String context = "\nContent-Type: multipart/mixed;
charset= GB2312\n" +super.getContext();
//同时对邮件进行HTML检查,是否有类似未关闭的标签
context = context + "\n邮件格式为:超文本格式";
return context;
}
}

优秀一点的邮件客户端会对邮件的格式进行检查,比如编写一封超文本格式的邮件,在内容中加上了标签,但是遗忘了结尾标签,邮件的产生者(也就是邮件的客户端)会提示进行修正,我们这里用了“邮件格式为:超文本格式”来代表该逻辑。

两个实现类实现了不同的算法,给定相同的发件人、收件人、标题和内容可以产生不同的邮件信息。我们看看邮件是如何发送出去的,如代码清单33-4所示。

代码清单33-4 邮件服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MailServer {
//发送的是哪封邮件
private MailTemplate m;
public MailServer(MailTemplate _m){
this.m = _m;
}
//发送邮件
public void sendMail(){
System.out.println("====正在发送的邮件信息====");
//发件人
System.out.println("发件人:" + m.getFrom());
//收件人
System.out.println("收件人:" + m.getTo());
//标题
System.out.println("邮件标题:" + m.getSubject());
//邮件内容
System.out.println("邮件内容:" + m.getContext());
}
}

很简单,邮件服务器接收了一封邮件,然后调用自己的发送程序进行发送。可能读者要问了,为什么不把sendMail方法移植到邮件模板类中呢?这也是邮件模板类的一个行为,邮件可以被发送。是的,这确实是邮件的一个行为,完全可以这样做,两者没有什么区别,只是从不同的角度看待该方法而已。我们继续看场景类,如代码清单33-5所示。

代码清单33-5 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//创建一封TEXT格式的邮件
MailTemplate m = new HtmlMail("a@a.com","b@b.com","外星人攻击地球了","结局是外星人被地球人打败了!");
//创建一个Mail发送程序
MailServer mail = new MailServer(m);
//发送邮件
mail.sendMail();
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
====正在发送的邮件信息==== 
发件人:a@a.com
收件人:b@b.com
邮件标题:外星人攻击地球了
邮件内容:
Content-Type: multipart/mixed;charset=GB2312
结局是外星人被地球人打败了!
邮件格式为:超文本格式

当然,如果想产生一封文本格式的邮件,只要稍稍修改一下场景类就可以了:new HtmlMail修改为new TextMail,读者可以自行实现,非常简单。在该场景中,我们使用策略模式实现两种算法的自由切换,它提供了这样的保证:封装邮件的两种行为是可选择的,至于选择哪个算法是由上层模块决定的。策略模式要完成的任务就是提供两种可以替换的算法。

33.1.2 桥梁模式实现邮件发送

桥梁模式关注的是抽象和实现的分离,它是结构型模式,结构型模式研究的是如何建立一个软件架构,下面我们就来看看桥梁模式是如何构件一套发送邮件的架构的,如图33-3所示。

类图中我们增加了SendMail和Postfix两个邮件服务器来实现类,在邮件模板中允许增加发送者标记,其他与策略模式都相同。我们在这里已经完成了一个独立的架构,邮件有了, 发送邮件的服务器也具备了,是一个完整的邮件发送程序。需要读者注意的是,SendMail类不是一个动词行为(发送邮件),它指的是一款开源邮件服务器产品,一般*nix系统的默认邮件服务器就是SendMail;Postfix也是一款开源的邮件服务器产品,其性能、稳定性都在逐步赶超SendMail。

image-20210930202528940

图33-3 桥梁模式实现邮件发送的类图

我们来看代码实现,邮件模板仅仅增加了一个add方法,如代码清单33-6所示。

代码清单33-6 邮件模板

1
2
3
4
5
6
7
8
9
public abstract class MailTemplate {
/*
*该部分代码不变,请参考代码清单33-1
*/
//允许增加邮件发送标志
public void add(String sendInfo){
context = sendInfo + context;
}
}

文本邮件、超文本邮件都没有任何改变,如代码清单33-2、33-3所示,这里不再赘述。

我们来看邮件服务器,也就是桥梁模式的抽象化角色,如代码清单33-7所示。

代码清单33-7 邮件服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class MailServer {
//发送的是哪封邮件
protected final MailTemplate m;
public MailServer(MailTemplate _m){
this.m = _m;
}
//发送邮件
public void sendMail(){
System.out.println("====正在发送的邮件信息====");
//发件人
System.out.println("发件人:" + m.getFrom());
//收件人
System.out.println("收件人:" + m.getTo());
//标题
System.out.println("邮件标题:" + m.getSubject());
//邮件内容
System.out.println("邮件内容:" + m.getContext());
}
}

该类相对于策略模式的环境角色有两个改变:

  • 修改为抽象类。为什么要修改成抽象类?因为我们在设计一个架构,邮件服务器是一 个具体的、可实例化的对象吗?“给我一台邮件服务器”能实现吗?不能,只能说“给我一台 Postfix邮件服务器”,这才能实现,必须有一个明确的可指向对象。
  • 变量m修改为Protected访问权限,方便子类调用。

我们再来看看Postfix邮件服务器的实现,如代码清单33-8所示。

代码清单33-8 Postfix邮件服务器

1
2
3
4
5
6
7
8
9
10
11
12
public class Postfix extends MailServer {
public Postfix(MailTemplate _m) {
super(_m);
}
//修正邮件发送程序
public void sendMail(){
//增加邮件服务器信息
String context ="Received: from XXXX (unknown [xxx.xxx.xxx.xxx]) by aaa.aaa.com (Postfix) with ESMTP id 8DBCD172B8\n" ;
super.m.add(context);
super.sendMail();
}
}

为什么要覆写sendMail程序呢?这是因为每个邮件服务器在发送邮件时都会在邮件内容上留下自己的标志,一是广告作用,二是为了互联网上统计需要,三是方便同质软件的共振。我们再来看SendMail邮件服务器的实现,如代码清单33-9所示。

代码清单33-9 SendMail邮件服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SendMail extends MailServer {
//传递一封邮件
public SendMail(MailTemplate _m) {
super(_m);
}
//修正邮件发送程序
@Override
public void sendMail(){
//增加邮件服务器信息
super.m.add("Received: (sendmail);7 Nov 2009 04:14:44 +0100");
super.sendMail();
}
}

邮件和邮件服务器都有了,我们来看怎么发送邮件,如代码清单33-10所示。

代码清单33-10 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//创建一封TEXT格式的邮件
MailTemplate m = new HtmlMail("a@a.com","b@b.com","外星人攻击地球了"," 结局是外星人被地球人打败了!");
//使用Postfix发送邮件
MailServer mail = new Postfix(m);
//发送邮件
mail.sendMail();
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
====正在发送的邮件信息==== 
发件人:a@a.com
收件人:b@b.com
邮件标题:外星人攻击地球了
邮件内容: Content-Type: multipart/mixed;charset=GB2312
Received: from XXXX (unknown [xxx.xxx.xxx.xxx]) by aaa.aaa.com (Postfix) with ESMTP id 8DBCD172B8
结局是外星人被地球人打败了!
邮件格式为:超文本格式

当然了,还有其他三种发送邮件的方式:Postfix发送文本邮件以及SendMail发送文本邮件和超文本邮件。修改量很小,读者可以自行修改实现,体会一下桥梁模式的特点。

33.1.3 最佳实践

策略模式和桥梁模式是如此相似,我们只能从它们的意图上来分析。策略模式是一个行为模式,旨在封装一系列的行为,在例子中我们认为把邮件的必要信息(发件人、收件人、 标题、内容)封装成一个对象就是一个行为,封装的格式(算法)不同,行为也就不同。而桥梁模式则是解决在不破坏封装的情况下如何抽取出它的抽象部分和实现部分,它的前提是不破坏封装,让抽象部分和实现部分都可以独立地变化,在例子中,我们的邮件服务器和邮件模板是不是都可以独立地变化?不管是邮件服务器还是邮件模板,只要继承了抽象类就可以继续扩展,它的主旨是建立一个不破坏封装性的可扩展架构。

简单来说,策略模式是使用继承和多态建立一套可以自由切换算法的模式,桥梁模式是在不破坏封装的前提下解决抽象和实现都可以独立扩展的模式。桥梁模式必然有两个“桥墩”——抽象化角色和实现化角色,只要桥墩搭建好,桥就有了,而策略模式只有一个抽象角色,可以没有实现,也可以有很多实现。

还是很难区分,是吧?多想想两者的意图,就可以理解为什么要建立两个相似的模式了。我们在做系统设计时,可以不考虑到底使用的是策略模式还是桥梁模式,只要好用,能够解决问题就成,“不管黑猫白猫,抓住老鼠的就是好猫”。

34.2 混编小结

在这里的例子中用到了以下模式。

  • 责任链模式

负责对命令的参数进行解析,而且所有的扩展都是增加链数量和节点,不涉及原有的代码变更。

  • 命令模式

负责命令的分发,把适当的命令分发到指定的链上。

  • 模板方法模式

在Command类以及子类中,buildChain方法是模板方法,只是没有基本方法而已;在责任链模式的CommandName类中,用了一个典型的模板方法handlerMessage,它调用了基本方法,基本方法由各个实现类实现,非常有利于扩展。

  • 迭代器模式

在for循环中我们多次用到类似for(Class c:classes)的结构,是谁来支撑该方法运行?当然是迭代器模式,只是JDK已经把它融入到了API中,更方便使用了。

可能读者已经注意到了,”ls-l-a”这样的组合选项还没有处理。确实没有处理,以下提供两个思路来处理。

  • 独立处理

“ls-l-a”等同于”ls-la”,也等同于”ls-al”命令,可以把”ls-la”中的选项”la”作为一个参数来 进行处理,扩展一个类就可以了。该方法的缺点是类膨胀得太大,但是简单。

  • 混合处理

修正命令族处理链,每个命令处理节点运行完毕后,继续由后续节点处理,最终由Command类组装结果,根据每个节点的处理结果,组合后生成完整的返回信息,如”ls-l-a”就应该是LS_L类与LS_A类两者返回值组装的结果,当然链上的节点返回值就要放在Collection 类型中了。

该框架还有一个名称,叫做命令链(Chain of Command)模式,具体来说就是命令模式作为责任链模式的排头兵,由命令模式分发具体的消息到责任链模式。对于该框架,读者可以继续扩展下去。当然,上面的程序还可以优化,优化的结果就是Command类缩为一个类, 通过CommandEnum配置文件类传递命令,这比较容易实现,读者可以自行设计。

35.2 混编小结

回顾一下我们在该案例中使用了几个模式。

  • 策略模式

负责对扣款策略进行封装,保证两个策略可以自由切换,而且日后增加扣款策略也非常简单容易。

  • 工厂方法模式

修正策略模式必须对外暴露具体策略的问题,由工厂方法模式直接产生一个具体策略对象,而其他模块则不需要依赖具体的策略。

  • 门面模式

负责对复杂的扣款系统进行封装,封装的结果就是避免高层模块深入子系统内部,同时提供系统的高内聚、低耦合的特性。

我们主要使用了这三个模式,它们的好处是灵活、稳定,我们可以设想一下可能有哪些业务变化。

  • 扣款策略变更

增加一个新扣款策略,三步就可以完成:实现IDeduction接口,增加StrategyMan配置项,扩展扣款策略的利用(也就是门面模式的getDeductionType方法,在实际项目中这里只需要增加数据库的配置项)。减少一个策略很简单,修改扣款策略的利用即可。变更一个扣款策略也很简单,扩展一个实现类口就可以了。

  • 变更扣款策略的利用规则

我们的系统不想大修改,还记得我们提出的状态模式吗?这个就是为策略的利用服务的,变更它就能满足要求。想把IC卡也纳入策略利用的规则也不复杂。其实这个变更还真发生了,系统投产后,业务提出考虑退休人员的情况,退休人员的IC卡与普通在职员工一样, 但是它的扣款不仅仅是根据交易编码,还要根据IC卡对象,系统的变更做法是增加一个扣款策略,同时扩展扣款利用策略,也就是数据库的配置项,在getDeductionType中新扩展了一个功能:根据IC卡号,确认是否是退休人员,是退休人员,则使用新的扣款策略,这是一个非常简单的扩展。

这就是一个mini版的金融交易系统,没啥复杂的,剩下的问题就是开始考虑系统的鲁棒性,这才是难点。

36.2 混编小结

该事件触发框架结构清晰,扩展性好,读者可以进行抽象化处理后应用于实际开发中。 我们回头看看在这个案例中使用了哪些设计模式。

  • 工厂方法模式

负责产生产品对象,方便产品的修改和扩展,并且实现了产品和工厂的紧耦合,避免产品随意被创建而无触发事件的情况发生。

  • 桥梁模式

在产品和事件两个对象的关系中我们使用了桥梁模式,如此设计后,两者都可以自由地扩展(前提是需要抽取抽象化)而不会破坏原有的封装。

  • 观察者模式

观察者模式解决了事件如何通知处理者的问题,而且观察者模式还有一个优点是可以有多个观察者,也就是我们的架构是可以有多层级、多分类的处理者。想重新扩展一个新类型 (新接口)的观察者?没有问题,扩展ProductEvent即可。

  • 中介者模式

事件有了,处理者也有了,这些都会发生变化,并且处理者之间也有耦合关系,中介者则可以完美地处理这些复杂的关系。

我们再来思考一下,如果我们要扩展这个框架,可能还会用到什么模式?首先是责任链模式,它可以帮助我们解决一个处理者处理多个事件的问题;其次是模板方法模式,处理者的启用、停用等,都可以通过模板方法模式来实现;再次是装饰模式,事件的包装、处理者功能的强化都会用到装饰模式。当然了,我们还可能用到其他的模式,只要能够很好地解决我们的困境,那就好好使用吧,这也是我们学习设计模式的目的。