The Zen of Design Patterns (2nd Edition)

让设计模式成为一种习惯


第一章 单一职责原则

缩略语:

  • SRP(Single Responsibility Principle,单一职责模式)
  • RBAC(Role-Based Access Control, 基于角色的访问控制)
  • BO(Business Object,业务对象),负责用户的属性
  • Biz(Business Logic,业务逻辑),负责用户的行为

单一职责原则(SRP)指的是只有一个原因引起类的变更。

具体实践中应保证接口和方法一定做到单一职责,而类尽量做到。

第二章 里氏替换原则

缩略语:

  • LSP(Liskov Substitution Principle,里氏替换原则)

里氏替换原则(LSP)是指只要父类能出现的地方子类都能出现。

实现里氏替换原则需要做到:

  1. 子类必须完全实现父类的方法
  2. 子类可以有自己的个性
  3. 覆写(Override)或实现父类方法时输入参数可以被放大
  4. 覆写(Override)或实现父类方法时输出结果可以被缩小

第三条指的是子类中的方法的前置条件必须与超类中被覆写方法的前置条件相同或更宽松。这是覆写的要求,也是重中之重。否则若方法的输入参数被缩小,则子类在没有覆写父类方法的前提下,子类方法可能被执行了,会引起业务逻辑混乱,歪曲了父类的意图。

注意输入参数不同就不能称为覆写(Override),加上@Override会出错,应该称为重载(Overload)。

第三章 依赖倒置原则

缩略语:

  • DIP(Dependence Inversion Principle,依赖倒置原则)
  • OOD(Object-Oriented Design,面向对象设计,面向接口编程)
  • TDD(Test-Driven Development,测试驱动开发)

依赖倒置是指面向接口编程(OOD)。依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面向实现编程。我要开奔驰车就依赖奔驰车。而抽象间的依赖代替了人们传统思维的事物间的依赖,“倒置”就是从这里产生的。

实现依赖倒置原则需要做到:

  1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象。即类之间不发生直接的依赖关系,其依赖是通过接口或抽象类产生的
  2. 抽象不应该依赖细节。即接口或抽象类不依赖于实现类
  3. 细节应该依赖抽象。即实现类依赖于接口或抽象类

依赖的三种写法:

  1. 构造函数传递依赖对象
Public interface IDriver {
    public void drive();
}
public class Driver implements IDriver {
    private ICar car;
    // 构造函数注入
    public Driver(ICar _car) {
        this.car = _car;
    }
    public void drive() {
        this.car.run();
    }
}
  1. Setter方法传递依赖对象
public interface IDriver {
    public void setCar(Icar car);
    public void drive();
}
public class Driver implements IDriver {
    private ICar car;
    // Setter注入
    public void setCar(ICar _car) {
        this.car = _car;
    }
    public void drive() {
        this.car.run();
    }
}
  1. 接口声明传递依赖对象,也叫做接口注入。
public interface IDriver {
    // 接口注入
    public void drive(ICar car);
}
public class Driver implements IDriver {
    public void drive(ICar car) {
        car.run();
    }
}

有依赖关系的不同开发人员,如甲负责IDriver,乙负责ICar,则两个开发人员只要定好接口就可以独立开发,并可以独立进行单元测试。这也就是测试驱动开发(TDD),是依赖倒置原则的最高级应用。比如可以引入JMock工具,根据抽象虚拟一个对象进行测试。

Public class DriverTest extends TestCase {
    Mockery context = new JUnit4Mockery();
    @Test
    public void testDriver() {
        // 根据接口虚拟一个对象
        final ICar car = context.mock(ICar.class);
        IDriver driver = new Driver();
        // 内部类
        context.checking(new Expectations(){{
            oneOf(car).run();
        }});
        driver.drive(car);
    }

}

具体实践中,实现依赖倒置需要遵守:

  1. 每个类尽量都有接口或抽象类,或者二者兼备
  2. 变量的表面类型尽量是接口或抽象类,工具类和需要使用clone方法的类除外
  3. 任何类都不应该从具体类派生
  4. 尽量不要覆写基类的方法。如果基类是抽象类,且该方法已经实现,则子类尽量不要覆写
  5. 综合里氏替换原则

第四章 接口隔离原则

缩略语

  • ISP(Interface Segregation Principle,接口隔离原则)

接口隔离原则指的是类间的依赖关系应该建立在最小的接口上。即接口尽量细化,同时接口中的方法尽量少。

这与单一职责原则的审视角度是不同的,单一职责原则是业务逻辑的划分,而接口隔离原则要求接口的方法尽量少。提供几个模块就应该有几个接口,而不是建立一个庞大臃肿的接口容纳所有的客户端访问。注意,根据接口隔离原则拆分接口时,必须满足单一职责原则。

第五章 迪米特原则

缩略语:

  • LoD(Law of Demeter,迪米特原则),也称为LKP(Least Knowledge Principle,最少知识原则)
  • RMI(Remote Method Invocation,远程方法调用)
  • VO(Value Object,值对象)

迪米特原则指的是一个类应该对自己需要耦合或调用的类知道的最少,不关心其内部的具体实现,只关心提供的public方法。即类间解耦,弱耦合。

迪米特原则要求:

  1. 只与直接的朋友通信。出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。一个方法中尽量不引入一个类中不存在的对象,JDK API提供的类除外
  2. 朋友间也是有距离的。尽量不对外公布太多的public方法和非静态的public变量
  3. 是自己的就是自己的。如果一个方法放在本类中,既不增加类间关系,对本类也不产生负面影响,那就放置在本类中
  4. 谨慎使用Serializable。在一个项目中使用远程方法调用(RMI),传递一个值对象(VO),这个对象就必须实现Serializable接口(仅仅是一个标志性接口,不需要实现具体方法),否则就会出现NotSerializableException异常。当客户端VO修改了一个属性的访问权限,由private变更为public,访问权限扩大了,如果服务器上没有做出相应的变更,就会出现序列化失败。当然,这属于项目管理中客户端与服务器同步更新的问题。

在具体实践中,既要做到高内聚低耦合,又要让结构清晰。如果一个类跳转两次以上才能访问另一个类,则需要考虑重构了,这就是过度地解耦了。因为跳转次数越多,系统越复杂,维护就越难。

第六章 开闭原则

缩略语:

  • OCP(Open Closed Principle,开闭原则)

开闭原则指的是软件实体如类、模块和方法应该对扩展开放,对修改关闭。这是Java世界中最基础的设计原则,以建立一个稳定灵活的系统。也就是说,一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。并不意味着不做任何修改,在业务规则改变或低层次模块变更的情况下高层模块必须有部分改变以适应新业务,改变要尽可能地少,防止变化风险的扩散。

一个方法的测试方法一般不少于3种,有业务逻辑测试,边界条件测试,异常测试等。

实现开闭原则需要做到:

  1. 抽象约束。在子类中不允许出现接口或抽象类中不存在的public方法;参数类型、引用类型尽量使用接口或抽象类而不是实现类;抽象类尽量保持稳定,一旦确定不允许修改
  2. 元数据(metadata)控制模块行为。元数据为描述环境和数据的数据,即配置参数。元数据控制模块行为的极致为控制反转(Inversion of Control),使用最多的就是Spring容器
  3. 制定项目章程。如所有的Bean都自动注入,使用Annotation进行装配,进行扩展时,只用可以只用一个子类,然后由持久层生成对象
  4. 封装可能的变化。将相同的变化封装到一个接口或抽象类中;将不同的变化封装到不同的接口或抽象类中

软件设计最大的难题就是应对需求的变化。6大设计原则和23个设计模式都是为了封装未来的变化。

6大设计原则有:

  1. Single Responsibility Principle: 单一职责原则
  2. Open Closed Principle: 开闭原则
  3. Liskov Substitution Principle: 里氏替换原则
  4. Law of Demeter: 迪米特原则
  5. Interface Segregation Principle: 接口隔离原则
  6. Dependence Inversion Principle: 依赖倒置原则

将6个原则的首字母联合起来,就是Solid(稳定的),里氏替换原则和迪米特原则的首字母都是L,只取一个。开闭原则是最基础的原则,是精神口号,其他5大原则是开闭原则的具体解释。

第七章 单例模式

单例模式(Singleton Pattern)指的是确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。其实现方式可以分为饿汉式和懒汉式。

1-singleton

单例模式的优点有:

  1. 减少内存开支,特别是当一个对象需要频繁创建和销毁时
  2. 减少了系统的性能开销
  3. 避免对资源的多重占用
  4. 可以在系统设置全局的访问点,优化共享资源的访问

在具体实践中,Spring的每个Bean默认就是单例的,这样Sring容器可以管理这些Bean的生命期,决定何时创建、何时销毁和如何销毁等。

第八章 工厂方法模式

工厂方法模式指的是定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

2-factory

工厂方法模式的优点有:

  1. 有良好的封装性,代码结构清晰。调用者需要创建一个具体产品对象时,只需要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的过程,降低模块间的耦合性
  2. 优秀的扩展性,在增加产品类的情况下,只需要适当修改或扩展工厂类即可
  3. 屏蔽产品类,不需要关心产品类的实现,只关心产品类的接口
  4. 工厂方法是典型的解耦框架,符合迪米特原则(最少知识原则),符合依赖倒置原则,也符合里氏替换原则

在具体实践中,工厂方法模式可以缩小为简单工厂模式,也叫做静态工厂模式。即将工厂类去掉继承抽象类,并添加具体生产方法前添加static关键字,其缺点是扩展比较困难,不符合开闭原则。

工厂方法模式还可以升级为多个工厂类,当然此时如果要扩展一个产品类,就需要建立一个相应的工厂类,这就增加了扩展的难度,所以一般会再添加一个协调类,用来封装子工厂类,为高层模块添加统一的访问接口。

工厂方法模式也可以替代单例模式,不过需要在工厂类中使用反射的方式建立单例对象。

工厂方法模式还可以延迟初始化,即一个对象被消费完毕之后并不立刻释放,工厂类会保持其初始状态,等待再次被使用。延迟加载框架是可以扩展的,比如限制某一产品类的最大实例化数量(数据库的最大连接数量等),可以通过判断Map中已有的对象数量来实现。

第九章 抽象工厂方法

抽象工厂模式指的是为创建一组相关的或相互依赖的对象提供一个接口,而且无需指定它们的具体类。

3-abstract-factory

抽象工厂模式的优点有:

  1. 封装性
  2. 产品族内的约束为非公开状态,例如每生产一个产品A,就同时生产出1.2个产品B,这个约束是在工厂内实现的,对高层模块来说是透明的。

抽象工厂模式是工厂方法模式的升级版本。即拥有两个或两个以上互相影响的产品族,或者说产品的分类拥有两个或两个以上的维度,如不同性别和肤色的人。如果拥有两个维度,则抽象工厂模式比工厂方法模式多一个抽象产品类,以维护两个分类维度,即不同的抽象产品类维护一个维度,而不同的产品实现类维护另外一个维度;而抽象工厂类不增加,但需要增加不同的工厂实现类。

抽象工厂模式的缺点是产品族的扩展非常困难。例如增加一个产品C,则抽象类AbstractCreator要增加一个方法createProductC(),然后两个实现类都要修改。这样就违反了开闭原则。而另外一个维度产品等级是很容易扩展的,对于工厂类只需要增加一个实现类即可。也就是说,抽象工厂模式横向扩展容易,纵向扩展困难。对于横向,抽象工厂模式是符合开闭原则的。

第十章 模板方法模式

模板方法模式值得是定义一个操作中的算法框架,而将一些步骤的实现延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

4-template

模板方法模式的抽象模板类一般包含两类方法,一种是基本方法,由子类实现,并且在模板方法中被调用;另一种是抽象方法,可以有一个或多个,一般是一个具体方法,也就是一个框架,实现对基本方法的调度,完成固定的逻辑。模板方法一般会加上final关键字,不允许覆写,以防恶意操作。

如果模板方法的执行结果受到基本方法的返回值或所设置的值的影响,则该基本方法称为钩子方法(Hook Method),即钩子方法可能会影响其公共部分(模板方法)的执行顺序。

模板方法模式的优点有:

  1. 封装不变部分,扩展可变部分
  2. 提取公共部分代码,便于维护。相同的一段代码如果复制过两次,就需要对设计产生怀疑
  3. 行为由父类控制,子类实现,符合开闭原则

模板方法的缺点是子类的实现会影响父类的结果,也就是子类会对父类产生影响,在复杂项目中,会增加代码阅读的难度。

在具体实践中,如果被问到“父类如何调用子类的方法”,其实现方式可能有把子类传递到父类的有参构造中,然后调用;使用反射的方式调用;父类调用子类的静态方法。当然项目中强烈不建议这么做,如果要调用子类的方法,可以使用模板方法模式,影响父类行为的结果。

第十一章 建造者模式

建造者模式也称为生成器模式,指的是将一个复杂对象的构建和它的表示分离,使得同样的构建过程可以创建不同的表示。

5-builder

通常,建造者模式有4个角色,产品类,抽象建造者,建造者的实现类(如可以传入产品的模块顺序或数量配置,并生产产品),导演类(如负责安排已有模块的顺序或数量,然后告诉建造者开始建造)。

建造者模式的优点:

  1. 封装性
  2. 建造者独立,容易扩展
  3. 便于控制细节风险

建造者模式关注的是零件类型和装配工艺(顺序),这是它与工厂方法模式最大的不同之处,虽然同为创建类模式,但关注点不同。工厂方法的重点是创建零件,而组装顺序不是它关心的内容。

在具体实践中,使用创建者模式的时候考虑一下模板方法模式,二者的配合可以作为创建者模式的扩展。

第十二章 代理模式

缩略语:

AOP(Aspect Oriented Programming,面向横切面编程)

代理模式又称为委托模式,指的是为其他对象提供一种代理以控制对这个对象的访问。其他的许多模式,如状态模式、策略模式和访问者模式本质上是在更特殊的场合使用了代理模式。

6-proxy

代理类不仅可以实现主题接口,还可以为真实角色预处理信息、过滤信息、消息转发、事后处理信息等功能。

代理模式的优点有:

  1. 职责清晰。真实的角色只实现实际的业务逻辑,而通过后期的代理完成某一件事务
  2. 高扩展性
  3. 智能化,例如动态代理

代理模式可以扩展为普通代理,要求客户端只能访问代理角色,而不能访问真实角色,真实角色由代理角色来实例化。

代理模式还可以作为强制代理,这个比较另类,所谓强制表示必须通过真实角色找到代理角色,否则不能访问。也就是说高层模块new了一个真实角色的对象,返回的却是代理角色。比如拨通明星的电话,确强制返回其经纪人作为代理,这就是强制代理。

代理模式还可以作为动态代理,动态代理是在实现阶段不用关心代理谁,在运行阶段才指定代理哪一个对象。相对来说,自己写代理类的方式就是静态代理。面向横切面编程(AOP)的核心就是动态代理机制。

一个类的动态代理类由InvocationHandler(JDK提供的动态代理接口)的实现类通过invoke方法实现所有的方法,

在具体实践中,关于AOP框架,需要弄清楚几个名词,包括切面(Aspect)、切入点(JoinPoint)、通知(Advice)和织入(Weave),这样在应用中就游刃有余了。

第十三章 原型模式

原型模式指的是用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。即不通过new关键字来产生一个对象,而是通过对象复制来实现。可以理解为一个对象的产生可以不由零起步,而是由正本通过二进制内存拷贝创建多个副本,然后再修改为生产需要的对象。

7-prototype

原型模式的核心是一个clone方法,通过该方法进行对象的拷贝,Java提供了一个Cloneable接口来标识这个对象是可拷贝的,且还必须覆写clone方法。为什么说Cloneable接口仅作为标识还称clone方法为覆写,因为clone方法是Object类的,而每个类都默认继承了Object类。

原型模式的优点有:

  1. 性能优良,内存二进制流的拷贝
  2. 规避构造函数的约束,当然这个有时候也是缺点,直接内存拷贝,其构造函数是不会执行的

需要注意的是Object提供的clone方法只是拷贝对象,其对象内部的数组、引用对象都不拷贝,还是指向原生对象的内部元素地址,这种拷贝称为浅拷贝。两个对象共享了一个私有变量,并且都可以对其进行修改,这是一种很不安全的方式。

浅拷贝时,内部的数组和引用对象不拷贝,其他的基本类型如int、long和char等都会被拷贝,对于String类型,Java希望你把它当作基本类型,它是没有clone方法的,处理机制也比较特殊,通过字符串池(stringpool)在需要的时候才在内存中创建新的字符串。

深拷贝则需要考虑成员变量中所有的数组和可变的引用对象,进一步调用或覆写引用对象的clone方法,如JDK提供的ArrayList的clone方法。

此外要使用clone方法,类的成员变量上不要增加final关键字。

在具体实践中,原型模式很少单独出现,一般和工厂方法模式一起出现,通过clone方法创建一个对象,然后由工厂方法提供给调用者。

第十四章 中介者模式

中介者模式指的是用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使其耦合松散,并且可以独立地改变它们之间的交互。中介者类似于星型网络拓扑中的中心交换机连接各计算机。也称为调停者模式。

8-mediator

中介者模式的优点就是减少了类间的依赖,把原有的一对多的依赖变成了一对一的依赖,同事类只依赖中介者,减少了依赖,当然同时也降低了类间的耦合。

中介者模式的缺点是中介者会膨胀得很大,而且同事类越多,逻辑越复杂。

在面向对象编程中,对象与对象之间必然存在依赖关系,如果某个类和其他类没有任何相互依赖的关系,那么这个类在项目中就没有存在的必要了。所以中介者模式适用于多个对象之间紧密耦合的情况,紧密耦合的标准是在类图中出现了蜘蛛网状结构。而中介者模式的使用可以将蜘蛛网结构梳理为星型结构。

在具体实践中,Struts的MVC框架中的C(Controller)就是一个中介者,叫做前端控制器。它的作用就是把M(Model,业务逻辑)和V(View,视图)隔离开来,减少M和V的依赖关系。

第十五章 命令模式

命令模式指的是将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,将多个请求排队或者记录请求日志,也可以提供命令的撤销和恢复功能。

9-command

命令模式的优点有:

  1. 类间解耦。调用者角色和接收者角色之间没有任何的依赖关系,调用者实现功能时只需调用Command抽象类execute方法就可以了,不需要了解到底是哪个接收者执行
  2. 可扩展性。Command的子类可以非常容易扩展
  3. 命令模式与其他模式的结合会更优秀。命令模式结合责任链模式,实现命令族解析任务;命令模式结合模板方法模式,则可以减少Command子类的膨胀问题

命令模式的缺点是如果命令很多,则Command子类很容易增多,这个类就显得非常膨胀

在具体实践中,高层模块可以不需要知道具体的接收者,而是用具体的Command子类将接收者封装起来,每个命令完成单一的职责,解除高层模块对接收者的依赖关系。

第十六章 责任链模式

责任链模式指的是使多个对象都有机会处理请求,从而避免了请求的发送者和接收者的耦合关系,将这些接收者对象连成一条链,并沿着这条链传递请求,知道有对象处理它为止。注意请求是由接收者对象来决定是否传递的,而不是在高层模块中判断。

10-chain-of-responsibility

责任链模式的优点是将请求和处理分开,请求者不用知道是谁处理的,只问了责任链中的第一个接收者,只需要得到一个回复,而关于请求是否传递则由接收者来传递。同样的,接收者也不用知道请求的全貌。两者解耦,提高系统的灵活性。

责任链的缺点一个是性能问题,责任链比较长的时候遍历会带来性能问题;另一个是调试不方便,由于采用了类似递归的方式,逻辑可能比较复杂。

在具体实践中,一般会有一个封装类对责任链模式进行封装,也就是替代场景类client,直接返回责任链中的第一个处理者,具体链的设置不需要高层模块知道,这样更简化了高层模块的调用,减少模块间的耦合性。另外,责任链的接收者节点数量需要控制,一般做法是在Handler中设置一个最大的节点数量,在setNext方法中判断是否超过该阈值。责任链模式通常会与模板方法结合,每个实现类只需要实现response方法和getHandlerLevel获取处理级别。

第十七章 装饰模式

装饰模式指的是动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式比由对象直接生成子类更为灵活。装饰模式可以任意选择所需要添加的装饰,下一次传递的被装饰对象为上一次装饰完毕的对象,都继承自同一个Component抽象类。装饰模式也可看成是特殊的代理模式。

11-decorator

装饰模式的优点有:

  1. 装饰类和被装饰类可以独立发展,而不会相互耦合。即Component类无须知道Decorator类,而Decorator类是从外部扩展Component类的功能,而不需要知道具体的Component类的构件。
  2. 装饰模式是继承关系的一个替代方案。不管装饰多少层,返回的对象仍是Component,实现的还是is-a关系。
  3. 装饰模式可以动态地扩展一个实现类的功能。

装饰模式的缺点是多层的装饰还是比较复杂的,因为可能剥离到最里面一层,才发现错误,所以尽量减少装饰类的数量,以降低系统的复杂度。

在具体实践中,装饰模式是对继承的一种有力补充。可以在高层模块中动态地给一个对象增加功能,这些功能也可以再动态地撤销。而继承是静态地给类增加功能。而且装饰模式的扩展性也非常好,比如要在继承关系Father、Sonh和GrandSon三层类中的Son增强一些功能,怎么办,如何评估对GrandSon造成的影响,特别是GrandSon有多个的时候,此时就可以通过建立SonDecorator类来修饰Son,很好地完成这次变更。

第十八章 策略模式

策略模式指的是定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。策略模式也是一种特殊的代理模式,使用一个代理类(Context封装类)来代理多个对象(Strategy抽象类的具体实现)。

12-strategy

策略模式的优点有:

  1. 策略(算法)可以自由替换
  2. 避免使用多重条件判断
  3. 扩展性良好

策略模式的缺点有策略类数量增多导致类膨胀,另一个是所有的策略类都需要对外暴露。上层模块必须知道有哪些策略,才能决定使用哪一个策略,这与迪米特原则是违背的,要你的封装类有何用。

在具体实践中,一般通过工厂方法模式来实现策略类的声明,来避免所有策略都必须对高层模块暴露的问题。

第十九章 适配器模式

适配器模式指的是将一个类的接口变换成客户端所期待的另一种接口,从而使原本不匹配的两个类能够共同工作。适配器模式又称为变压器模式。适配器模式也是包装模式的一种,包装模式还有装饰模式。

13-adapter

适配器模式的优点有:

  1. 让两个没有任何关系的类在一起运行
  2. 增加了类的透明性。我们访问的Target目标角色,其具体实现都委托给了源角色,而这些对高层模块是不可见的
  3. 提高了类的复用度。源角色在原有的系统中可以正常使用,通过适配器角色中也可以让Target目标角色使用
  4. 灵活性非常好。适配器角色删除后不会影响其他代码

在具体实践中,在细节设计阶段不要考虑适配器模式,适配器模式不是为了解决还处于开发阶段的问题,而是为了解决正在服役的项目问题。换句话说,适配器模式是一种补救模式。适配器可分为对象适配器和类适配器,类适配器是类间适配,通过继承关系适配源角色,而对象适配器是通过关联聚合关系适配多个源角色。在实际项目中,对象适配器更为灵活,使用的也较多。

第二十章 迭代器模式

迭代器模式指的是提供一种方法访问一个容器对象中各个元素,而又不需要暴露该对象的内部细节。迭代器是为容器服务的,能容纳对象的所有类型都可以称之为容器,例如Collection类型、Set类型等。

14-interator

Java的的迭代器接口java.util.iterator有三个方法hasNext、next和remove。迭代器的remove方法应该完成两个逻辑:一是删除当前元素,而是当前游标指向下一个元素。而抽象容器类必须提供一个方法来创建并返回具体迭代器角色,Java中通常为iterator方法。

在具体实践中,Java的基本API中的容器类基本都融入了迭代器模式,所以尽量不要自己写迭代器模式。

第二十一章 组合模式

组合模式指的是将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

15-composite

组合模式的优点有:

  1. 高层模块调用简单,一个树形结构中的所有节点都是Component,简化了高层模块的调用
  2. 节点自由增加

组合模式的缺点是叶子类和树枝类直接继承了Component抽象类,这与面向接口编程相冲突,即不符合依赖倒置的原则。

组合模式有两种不同的实现,一种是安全模式,一种是透明模式。安全模式是指叶子对象和树枝对象的结构不同,而公共部分的Operation()方法放到Component抽象类中。而透明模式指的是,叶子对象和树枝对象的结构相同,所有方法全部集合到Component抽象类中,通过getChildren的返回值判断是叶子节点还是树枝节点。

具体实践中,建议使用安全模式的组合模式。但透明模式基本遵循了依赖倒置的原则,方便系统进行扩展。页面结构、XML结构和公司的组织结构都是树形结构,都可以采用组合模式。

第二十二章 观察者模式

观察者模式指的是定义对象间一对多的依赖关系,使得当发布者对象的状态改变,则所有它依赖的订阅对象都会得到通知并自动更新。观察者模式也叫做发布/订阅模式。

16-observer

观察者模式的优点有:

  1. 被观察者与观察者之间是抽象耦合,容易扩展
  2. 建立了一套触发机制,很容易实现一条触发链

观察者模式的缺点是开发效率和运行效率的问题。一个被观察者和多个观察者,甚至会有多级触发情况,开发和调试就会相对比较复杂,而且一个观察者卡壳,会影响整体的执行效率。在这种情况下可以考虑异步的方式。

EJB(Enterprise JavaBean)中有3个类型的Bean,分别为Session Bean、Entity Bean和MessageDriven Bean(MDB),对于MDB,消息的发布者发布一个消息,也就是一个消息驱动Bean,通过EJB容器(一般是Message Queue消息队列)通知订阅者作出响应,这个就是观察者模式的升级版,或成为企业版。

在具体实践中,多级触发的广播链最多为两级,即消息最多传递两次。观察者模式的多级触发与责任链模式的区别是观察者模式传递的消息是可以随时更改的,而责任链模式传递的消息一般是保持不变的,如果需要改变,也只是在原有的消息上进行修正。JDK中提供了java.util.Observable实现类作为被观察者和java.util.Observer接口作为观察者。

第二十三章 门面模式

门面模式指的是要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口使得子系统更容易使用。门面模式也叫做外观模式。

17-facade

门面模式注重统一的对象,也就是提供一个门面对象作为访问子系统的接口,除了这个接口不允许任何访问子系统的行为发生。

门面模式的优点有:

  1. 减少系统的相互依赖。客户端所有的依赖都是对门面对象的依赖,与子系统无关。
  2. 提高了灵活性。
  3. 提高安全性。只能访问在门面对象上开通的方法。

门面模式的缺点是不符合开闭原则。在业务更改或出现错误时,需要修改门面对象的代码。

门面模式不应该参与子系统内的业务逻辑,如在一个方法中先调用了子系统的ClassA的方法,再调用子系统的ClassB的方法。门面对象只是提供访问子系统的一个路径而已,它不应该也不能参与具体的业务逻辑,否则子系统必须依赖门面对象才能被访问,违反了单一职责原则,也破坏了系统的封装性。对于这种情况,应该建立一个封装类,封装完毕后提供给门面对象。

在具体实践中,当算法或者业务比较复杂时,可以封装出一个或多个门面出来,项目的结构比较简单,而且扩展性良好。另外对于一个大项目,使用门面模式也可以避免低水平开发人员带来的风险。使用门面模式后,可以对门面进行单元测试,约束项目成员的代码质量。

第二十四章 备忘录模式

备忘录模式指的是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到原先保存的状态。

18-memento

备忘录模式的备忘录角色可以由原型模式的clone方法创建。发起人角色只要实现Cloneable接口就成。这样发起人角色融合就融合发起人角色和备忘录角色,此时备忘录管理员角色直接依赖发起人角色。当然也可以再精简掉备忘录管理员角色,使发起人自主备份和恢复。这不是“在该对象之外保存这个状态”,而是把状态保存在了发起人内部,但这仍然可视为备忘录模式的一种。此外使用原型模式还必须要考虑深拷贝和浅拷贝的问题,所以Clone方式的备忘录模式只适用于较简单的场景。

备忘录模式还可以备份多状态,多备份。可以使用Map来实现。对于多备份,建议设置Map的上限,否则系统很容易产生内存溢出的情况。

在具体实践中,最好使用备忘录模式来代替使用数据库的临时表作为缓存备份数据,后者加大了数据库操作的频繁度,把压力下放到了数据库。

第二十五章 访问者模式

访问者模式指的是封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。

19-visitor

访问者模式的优点有:

  1. 符合单一职责原则。具体元素角色负责数据的加载,而Vistor类负责报表的展现。
  2. 优秀的扩展性。
  3. 灵活性非常高。

访问者模式的缺点有具体元素对访问者公布细节,这个并不符合迪米特法则。其次是具体元素的变更比较困难,可能牵扯到多个Vistor的修改。最后是违背了依赖倒置的原则,访问者依赖的是具体元素而不是抽象元素,抛弃了对接口的依赖,这方面的扩展比较难。

在具体实践中,访问者模式还可以用于统计功能,因为访问者可以知道具体元素的所有细节。当然还可以同时存在多个访问者来实现不同的访问功能(展示、汇总等)。

注意谈到访问者模式肯定要谈到双分派,双分派是多分派的一个特例。Java是单分派语言,但可以用访问者模式支持双分派,单分派语言处理一个操作是根据方法执行者的名称(覆写,动态绑定)和方法接收到的参数(重载,静态绑定)决定的。

// 演员抽象类,方法的执行者,动态绑定,场景类中会举例说明其含义
public abstract class AbsActor {
    //重载act方法,方法接收到的参数,静态绑定,场景类中会举例说明其含义
    //演员都能够演一个角色
    public void act(Role role){
        System.out.println("演员可以扮演任何角色");
    }
    //可以演功夫戏
    public void act(KungFuRole role){
        System.out.println("演员都可以演功夫角色");
    }
}

// 青年演员和老年演员
public class YoungActor extends AbsActor {
    //年轻演员最喜欢演功夫戏
    public void act(KungFuRole role){
        System.out.println("最喜欢演功夫角色");
    }
}
public class OldActor extends AbsActor {
    //不演功夫角色
    public void act(KungFuRole role){
        System.out.println("年龄大了,不能演功夫角色");
    }
}

//场景类
public class Client {
    public static void main(String[] args) {
        //定义一个演员
        AbsActor actor = new OldActor();
        //定义一个角色
        Role role = new KungFuRole();
        //开始演戏
        actor.act(role);
        actor.act(new KungFuRole());
    }
}

运行结果是什么呢?运行结果如下所示:

演员可以扮演任何角色 年龄大了,不能演功夫角色

重载在编译时就根据传进来的参数决定要调用哪个方法,这是静态绑定,而方法的执行者actor是动态绑定的。

引入访问者模式后,将重载拿掉,全部以动态绑定得到我们期望的结果。

//AbsActor为访问者,Role为元素
public interface Role {
    //演员要扮演的角色
    public void accept(AbsActor actor);
}
public class KungFuRole implements Role {
    //武功天下第一的角色
    public void accept(AbsActor actor){
        actor.act(this);
    }
}
public class IdiotRole implements Role {
    //一个弱智角色,由谁来扮演
    public void accept(AbsActor actor){
        actor.act(this);
    }
}

//场景类
public class Client {
    public static void main(String[] args) {
        //定义一个演员
        AbsActor actor = new OldActor();
        //定义一个角色
        Role role = new KungFuRole();
        //开始演戏
        role.accept(actor);
    }
}

运行结果如下:

年龄大了,不能演功夫角色

双分派意味着方法的具体执行由执行者的类型和接收参数的类型决定,而单分派是由执行者的名称(编译的时候决定)和接收参数的类型决定,这就是二者的区别,Java是一个支持双分派的单分派语言。

访问者模式的目的是实现功能集中化,如一个统一的报表计算,UI呈现等。

第二十六章 状态模式

状态模式指的是允许一个对象在其内部状态改变时改变它的行为,从外部看起来就好像这个对象对应的类发生了改变一样。状态模式是一种对象行为型模式。

20-state

状态模式由状态角色和环境角色组成。

环境角色有两个不成文的约束,一是把状态对象声明为静态变量,有几个状态角色就声明几个静态变量;二是环境角色具有状态抽象角色定义的行为,具体执行使用委托方式。

状态模式的优点有:

  1. 结构清晰,避免了过多的条件语句的使用。
  2. 遵循开闭原则和单一职责原则,对扩展开放,对修改关闭。
  3. 封装性良好,外部的调用不用知道类内部如何实现状态和行为的变换。

状态模式的缺点是如果状态过多,则会产生太多的子类,出现类膨胀的问题。

状态模式在具体实践中适用于行为随状态改变而改变的场景,作为条件、分支判断语句的替代者。使用的对象的状态最好不超过5个。建造者模式还可以将状态之间的顺序切换再重新组装一下,建造者模式加上状态模式得到一个非常好的封装效果。

第二十七章 解释器模式

解释器模式是一种按照规定语法进行解析的方案,在现有的项目中使用较少。给定一门语言,定义它的文法的一种表示(表达式),并定义一个解释器,该解释器使用该表示啦解释语言中的句子。

21-interpreter

表达式分为终结符表达式(如某个变量,只关心自身的结果)和非终结符表达式(如加减乘除法则,只关心左右或附近表达式的结果),其实就是逐层递归的意思。

解释器模式的优点是扩展性良好,修改语法规则只需要修改相应的非终结表达式就可以了,若扩展语法,则只要增加非终结符类就可以了。

解释器模式的缺点是会容易引起类膨胀,递归调用的方式增加了调试难度,降低了执行效率。

解释器模式的在具体实践中适用于重复发生的问题,如对大量日志文件进行分析处理,由于日志格式不相同,但数据要素是相同的,这便是终结符表达式都相同,而非终结符表达式需要制定。此外,尽量不要在重要的模块中使用解释器模式,否则维护会是一个很大的问题,可以使用shell、JRuby、Groovy等脚本语言代替解释器模式,弥补Java编译型语言的不足。当准备使用解释器模式时,可以考虑Expression4J、MESP(Math Expression String Prser)、Jep等开元的数学解析工具包。

第二十八章 享元模式

享元模式(Flyweight Pattern)是池技术的重要实现方式,使用共享对象可有效支持大量的细粒度的对象,从而避免内存溢出。

22-flyweight

细粒度对象的信息分为两个部分:内部状态(intrinsic)和外部状态(extrinsic)。

内部状态是对象可共享出来的信息,不会随环境的改变而改变,如id,可以作为一个对象的动态附加信息,不必直接存在具体的某个对象中,属于共享的部分。

外部状态是对象得以依赖的一个标记,是随环境改变而改变的,不可共享的状态,如考试科目+考试地点的复合字符串,它是对象的是唯一的索引值。

外部状态一般需要设置为final类型,初始化时一次赋值,避免无意修改导致池混乱,特别是Session级的常量或变量。

享元模式的优点是可以大大减少应用程序创建的对象,降低程序内存的占用,增强程序的性能,但它同时页提高了系统的复杂性,需要分离出外部状态和内部状态,且外部状态不随内部状态改变而改变,否则会导致系统的逻辑混乱。

享元模式在具体实践中适用于系统中存在大量相似对象的情况以及需要缓冲池的场景。在使用中需要注意线程安全的问题,此外尽量使用Java的基本类型作为外部状态,可以大幅提高效率,如果把一个对象作为Map类的键值,一定要确保重写了equals和hashCode方法,只有hashCode值相等,并且equals返回的结果为ture,两个对象的key才相等。

Java中String类的intern方法就使用了享元模式。

虽然使用享元模式可以实现对象池,但是二者还是有比较大的差异。对象池着重在对象的复用上,池中的每个对象是可替换的,从同一个池中获取A对象和B对象对客户端来说是完全相同的,它主要解决“复用”。而享元模式主要解决对象的共享问题,如何建立多个可“共享”的细粒度对象是其关注的重点。

第二十九章 桥梁模式

桥梁模式也叫桥接模式,将抽象和实现解耦,使得二者可以独立地变化。抽象化角色(分为抽象类和具体类)引用实现角色(同样分为抽象类和具体类),或者说抽象角色的部分实现是由实现角色完成的。抽象化对象一般在构造函数中指定(聚合)实现对象。

23-bridge

桥梁模式的优点是将抽象和实现分离,解决继承的缺点,不再绑定在一个固定的抽象层次上,拥有更良好的扩展能力,用户不必关心实现细节。

桥梁模式在具体实践中适用于接口或抽象类不稳定的场景以及重用性较高且颗粒度更细的场景。当发现继承有N层时,可以考虑使用桥梁模式。对于比较明确不发生变化的,则通过继承来完成,若不能确定是否会发生变化的,则可以通过桥梁模式来完成。


The End.

zhlinh

Email: [email protected]

2017-11-05