架构师训练营学习总结——面向对象的设计模式【第三周】

发布于: 2020 年 06 月 18 日

1 设计模式的作用

2 设计模式的定义

设计模式是一种可重复使用的解决方案,每一种模式都描述了一种问题的通用解决方案。这种问题在我们的环境中,不停地出现。

一个设计模式的四个部分:

  • 模式名称——由少量的字组成的名称,有助于我们表达我们的设计。

  • 待解问题——描述了何时需要运用这种模式,以及运用模式的环境(上下文)。

  • 解决方案——描述了组成设计的元素(类和对象)以及他们的关系、职责以及合作。但这种解决方案是抽象的,它不代表具体的实现。

  • 结论——运用这种方案所带来的利与弊。主要是指它对系统的弹性、扩展性和可移植性的影响。

3 设计模式的分类

3.1 从功能分

创建型设计模式——对类的实例化过程的抽象

结构型设计模式——将类或者对象结合在一起形成更大的结构

行为型设计模式——对在不同的对象之间划分责任和算法的抽象化

3.2 从方式分

从方式分,设计模式可分为两类:

  • 类模式:以继承的方式实现模式,静态的

  • 对象模式:以组合的方式实现模式,动态的

4 常用设计模式

4.1 单例模式

4.1.1 单例模式定义

单例模式:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

单例模式的类图如下图所示

单例模式

4.1.2 单例模式的优点

  • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

  • 由于单例模式只产生一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存方式来解决。

  • 单例模式可以避免对资源的多重占用,例如一个文件写动作,由于只有一个实例存在内存中,避免对同一个资源的同时写操作。

  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问。

4.1.3 单例模式的缺点

  • 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例,接口或抽象类是不可能被实例化的。在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。

  • 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。

  • 单例模式与单一职责原则有冲突。一个类只应该实现一个逻辑,而不关心它是不是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。

4.1.4 单例模式的使用场景

在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以使用单例模式,具体的场景如下:

  • 要求生成唯一序列号的环境;

  • 在整个项目中需要一个共享访问点或共享数据;

  • 创建一个对象需要的资源过多,如要访问IO和数据库等资源;

  • 需要定义大量的静态常量和静态方法的环境,可以采用单例模式,当然也可以直接声明为static的方式。

4.1.5 单例模式的注意事项

首先,在高并发情况下,要注意单例模式的多线程问题。

其次,需要考虑对象的复制情况,单例类不要实现Clonable接口。

4.2 工厂方法模式

4.2.1 工厂方法模式的定义

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

工厂方法模式类图

4.2.2 工厂方法模式的优点

  1. 首先,良好的封装性,代码结构清晰。一个对象创建是有条件约束的,如一个调用者需要一个具体的产品对象,只要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的艰辛过程,降低模块间的耦合。

  2. 其次,工厂方法模式的扩展性非常优秀。在增加产品类的情况下,只要适当地修改具体的工厂类或扩展一工厂类,就可以完成“拥抱变化”。

  3. 再次,屏蔽产品类。这一特点非常重要,产品类的实现如何变化,调用者都不需要关心。它只需要关心产品的接口,只要接口保持不变,系统上层模块就不用发生变化。

  4. 最后,工厂方法是典型的解耦框架。高层模块只需要知道产品的抽象类,其他的实现类都不用关心,符合迪米特原则,我不需要的就不要去交流;也符合依赖倒置原则,只依赖产品类的抽象;当然也符合里氏替换原则,使用产品子类代替产品父类,没问题。

4.2.3 工厂方法模式的使用场景

  1. 首先,工厂方法模式是new一个对象的替代品,所以所有需要生成对象的地方都可以使用,但是需要慎重地考虑是否要增加一个工厂类进行管理,增加代码的复杂度。

  2. 其次,需要灵活的、可扩展的框架时,可以考虑采用工厂方法模式。

  3. 再次,工厂方法模式可以用在异构项目中。

  4. 最后,可以使用在测试驱动开发的框架下。

4.2.4 工厂方法模式的扩展

工厂方法模式有很多扩展,而且与其他模式结合使用威力更大,下面介绍4种扩展

  1. 缩小为简单工厂模式(静态工厂模式)

  2. 升级为多个工厂类

  3. 替代单例模式

  4. 延迟初始化。何为延迟初始化?一个对象被消费完毕后,并不立即释放,工厂类保持其初始状态,等待再次被调用。

4.3 抽象工厂模式

4.3.1 抽象工厂模式的定义

为创建一组相关或相互依赖的对象创建一个接口,而且无须指定他们的具体类。

抽象工厂模式类图

4.3.2 抽象工厂模式的优点

  • 封装性,每个产品的实现类不是高层模块要关心的,他要关心的是什么,是接口,是抽象,它不关心对象是如何创建出来,这由谁负责呢?工厂类,只要知道工厂类是谁,就能创建出一个需要的对象。

  • 产品族内的约束为非公开状态。

4.3.3 抽象工厂模式的缺点

扩展产品族非常困难。

4.3.4 抽象工厂模式的使用场景

一个对象族(或者一组没有任何关系的对象),有相同的约束,则可以使用抽象工厂模式。

抽象工厂模式的类图如下图所示

抽象工厂模式类图

4.3.5 抽象工厂模式的注意事项

在抽象工厂模式的缺点中,我们提到抽象工厂模式的产品族比较困难,但是一定要清楚是产品族扩展困难,而不是产品等级。在该模式下,产品等级是非常容易扩展的,增加一个产品等级,只要增加一个工厂类负责新增加出来的产品生产任务即可。

4.4 模版方法模式

4.4.1 模版方法模式定义

定义一个操作中的算法的框架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的步骤即可重定义该算法的某些特定步骤。

为了防止恶意的操作,一般模版方法都加上final关键字,不允许被覆写。

模版方法模式的类图如下图所示

模板方法模式类图

4.4.2 模版方法模式的优点

  1. 封装不变部分,扩展可变部分。

  2. 提取公共部分代码,便于维护。

  3. 行为由父类控制,子类实现。

4.4.3 模版方法模式的缺点

按照我们的设计习惯,抽象类负责申明最抽象、最一般的事物属性和方法,实现类完成具体的事物属性和方法,但是模版方法模式却颠倒了,抽象类定义了部分抽象方法,由子类实现,子类执行的结果影响了父类的结果,也就是子类对父类产生了影响,这在复杂的项目中,会带来阅读代码的难度,而且也会让新手产生不适感。

4.4.4 模版方法模式的使用场景

  1. 多个子类有公有的方法,而且逻辑基本相同时;

  2. 重要、复杂的算法,可以把核心算法设计成模版方法,周边的相关细节功能则有各个子类实现。

  3. 重构时,模版方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数约束其行为。

4.4.5 模版方法模式的扩展

使用钩子方法控制公共部分的执行。

4.5 原型模式

4.5.1 原型模式的定义

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

原型模式的类图如下图所示

原型模式类图



实现Clone接口,然后重写clone方法,就完成了原型模式。原型模式通用代码如下所示。

public class PrototypeClass implements Cloneable{
// 覆写父类Object方法
@Override
public PrototypeClass clone(){
PrototypeClass prototypeClass = null;
try{
prototypeClass = (PrototypeClass)super.clone();
}
catch(CloneNotSupportedException e){
//异常处理
}
return prototypeClass;
}
}

4.5.2 原型模式的优点

  1. 性能优良。原型模式是在内存二进制流中的拷贝,要比直接new一个对象性能要好很多,特别是要在一个循环体类产生大量的对象时,原型模式可以更好地体现其优点。

  2. 逃避构造函数的约束。这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的,优点就是减少了约束,缺点也是减少了约束,需要大家在应用时考虑。

4.5.3 原型模式的使用场景

  1. 资源优化场景。类初始化需要消耗非常多的资源,这个资源包括数据,硬件资源等。

  2. 性能和安全要求的场景。通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。

  3. 一个对象多个修改者的场景。一个对象需要提供给其他对象访问,而且各个调用者可能都要修改其值时,可考虑用原型模式拷贝多个对象供调用者使用。

4.5.4 原型模式的注意事项

  1. 构造函数不会被执行

  2. 浅拷贝和深拷贝

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

4.6 迭代器模式

4.6.1 迭代器模式的定义

它提供一种方法访问一个容器对象中各个元素,而不需要暴露该对象的内部细节。

迭代器模式的类图如下图所示

迭代器模式类图

4.7 命令模式

4.7.1 命令模式的定义

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

命令模式的类图如下图所示

命令模式类图

4.7.2 命令模式的优点

  1. 类间解耦:调用者角色与接收者角色之间没有任何依赖关系,调用者使用功能时只需要调用Command类的execute方法就可以,不需要了解到底是哪个接受者执行。

  2. 可扩展性:Command的子类可以非常容易地扩展,而调用者Invoker和高层次的模块Client产生严重的代码耦合。

  3. 命令模式结合其他模式更优秀:命令模式可以结合责任链模式,实现命令族解析任务;结合模版方法设计模式,则可以减少Command子类的膨胀问题。

4.7.3 命令模式的缺点

命令模式也是有缺点的,如果有N个命令,Command的子类就有N个,这个类膨胀得非常大,这个就需要在项目中慎重考虑使用。

4.7.4 命令模式的使用场景

只要你认为有命令的地方,就可以采用命令模式,比如,一个按钮的点击是一个命令,可以采用命令模式;模拟DOS命令的时候,当然也要用命令模式;触发-反馈机制的处理等。

4.8 解释器模式(项目中用得很少)

4.8.1 解释器模式的定义

给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。

4.8.2 解释器模式的优点

解释器是一个简单语法分析工具,它最显著的优点就是扩展性,修改语法规则只要修改对应的非终结符表达式就可以了,若扩展语法,则只要增加非终结符就可以了。

4.8.3 解释器模式的缺点

  1. 解释器模式会引起类膨胀

  2. 解释器模式采用递归调用方法

  3. 效率问题

4.8.4 解释器模式使用的场景

  1. 重复发生的问题可以使用解释器模式

  2. 一个简单语法需要解释的场景

4.8.5 解释器模式的注意事项

尽量不要在重要的模块中使用解释器模式,否则维护会是一个很大的问题。在项目中可以采用 脚本语言来替代解释器模型。

4.9 责任链模式

4.9.1 责任链模式的定义

使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求对象,直到有对象处理它为止。

职责链模式类图

4.9.2 责任链模式的优点

责任链模式非常显著的优点是将请求和处理分开。请求者不知道是谁处理的,处理者可以不用知道请求的全貌,两者解耦,提高系统的灵活性。

4.9.3 责任链模式的缺点

责任链有两个非常显著的缺点:一是性能问题,每个请求都是从链头遍历到链尾,特别是在链长比较长的时候,性能是一个非常大的问题;二是调试不是很方便,特别是链长比较长、环节比较多的时候,由于采用了类似递归的方式,调试的时候逻辑可能比较复杂。

4.9.4 责任链模式的注意事项

链中节点数量需要控制,避免出现超长链的情况。一般的做法是在Handler中设置一个最大节点数量,在setNext方法中判断是否已经超过器阈值,超过则不允许该链建立,避免无意地破坏系统性能。

4.10 观察者模式

4.10.1 观察者模式的定义

观察者模式(Observer Pattern)也叫发布订阅模式(Publish/Subscribe)。定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。

观察者模式的类图下图所示

观察者模式类图

4.10.2 观察者模式的优点

  1. 观察者和被观察者之间是抽象耦合

  2. 建立一套触发机制

4.10.3 观察者模式的缺点

观察者模式,需要考虑一下开发效率和运行效率问题,一个被观察者,多个观察者,开发和调试就会比较复杂。而且在Java中,消息的通知默认是顺序执行,一个观察者卡壳,会影响整体的执行效率,在这种情况下,一般考虑采用异步的方式。多级触发时的效率更是让人但又,大家在设计时注意考虑。

4.10.4 观察者模式的使用场景

  1. 关联行为场景

  2. 事件多级触发场景

  3. 跨想通过的消息交换场景,如消息队列的处理机制

4.10.5 观察者模式的注意事项

  1. 广播链问题

  2. 异步处理问题

4.11 中介者模式

4.11.1 中介者模式的定义

用一个中介对象封装一系列的对象交互,中介者使各对象不需要显式地相互作用,而且可以独立地改变他们之间的交互。

4.11.2 中介者模式的优点

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

4.11.3 中介者模式的缺点

中介者模式的缺点就是中介者会膨胀得很大,而且逻辑复杂,原本N个对象直接的相互依赖关系转换为中介者与同事类的依赖关系,同事类越大,中介者的逻辑就越复杂。

4.12 备忘录模式

4.12.1 备忘录模式的定义

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

备忘录模式的类图如下图所示

备忘录模式

4.12.2 备忘录模式的使用场景

  1. 需要保存和恢复数据的相关状态场景

  2. 提供一个可回滚的操作

  3. 需要监控的副本场景。

  4. 数据库连接的事务管理就是用的备忘录模式。

4.12.3 备忘录模式的注意事项

  1. 备忘录的生命周期管理

  2. 备忘录的性能。不要频繁建立备份的场景中使用过备忘录模式,原因有二,一是控制不了备忘录建立的对象数量;二是大对象的建立是要消耗资源的,系统的性能需要考虑。

4.12.4 备忘录模式的扩展

  1. clone方式的备忘录

  2. 多状态的备忘录模式

  3. 多备份的备忘录模式

4.13 状态模式

4.13.1 状态模式的定义

当一个对象内在状态改变时允许改变行为,这个对象看起来像改变了它的类型。

4.13.2 状态模式的优点

  1. 结构清晰

  2. 遵循设计原则:开闭原则和单一职责原则

  3. 封装性非常好

4.13.3 状态模式的缺点

子类太多,类膨胀。

还有很多其他方式可以解决状态问题,比如在数据库中建立一个状态表,然后根据状态执行相应的操作。

4.13.4 状态模式的使用场景

  1. 行为随状态改变而改变的场景

  2. 条件、分支判断语句的替代者

4.13.5 状态模式的注意事项

状态模式适用于当某个对象在它的状态发生改变时,它的行为也随着发生比较大的变化,也就是说在行为受状态约束的情况下可以使用状态模式,而且使用时对象的状态最好不要超过5个。

4.14 策略模式

4.14.1 策略模式的定义

定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。

4.14.2 策略模式的优点

  1. 算法可以自由切换

  2. 避免使用多重田间判断

  3. 扩展性良好

4.14.3 策略模式的缺点

  1. 策略类数量增多

  2. 所有的策略类都需要对外暴露

4.14.4 策略模式的场景

  1. 多个类只有在算法或行为上稍有不通过的场景

  2. 算法需要自由切换的场景

  3. 需要屏蔽算法规则的场景

4.14.4 策略模式的注意事项

如果系统中的一个策略家族的具体策略数量超过4个,则需要考虑使用混合模式,解决策略类膨胀和对外暴露的问题,否则日后的系统维护将会很困难。

4.15 建造者模式

4.15.1 建造者模式的定义

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式的类图如下图所示

建造者模式类图

在建造者模式中,有如下4个角色:

  • Product产品类

通常是实现了模版方法模式,也就是有模版方法和基本方法。

  • Builder抽象建造者

规范产品的组件,一般是由子类实现。

  • ConcreteBuilder具体建造者

实现抽象类定义的所有方法,并且返回一个组件好的对象。

  • Director导演类

复杂安排已有模块的顺序,告诉Builder开始建造。

4.15.2 建造者模式的优点

  1. 封装性:使用建造者模式可以使客户端不必知道产品内部组成的细节

  2. 建造者独立,容易扩展

  3. 便于控制细节风险:由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其他的模块产生任何影响。

4.15.3 建造者模式的使用场景

  1. 相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式;

  2. 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使用该模式;

  3. 产品类非常复杂,或者产品类的调用顺序不同产生了不同的效能,这个时候使用建造者模式非常合适;

  4. 在对象的创建中会使用到系统中的一些其他对象,这些对象在产品对象的过程过程中不易得到时,也可以采用建造者模式封装该对象的创建过程。这种场景只能是一个补偿方法,因为一个对象不容易获得,而在设计阶段竟然没有发觉,而要通过创建者模式柔化创建过程,本身已经违反设计的最初目标。

4.15.4 建造者模式的注意事项

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

4.15.5 建造者模式的扩展

和模版方法模式设计。

4.16 访问者模式

4.16.1 访问者模式的定义

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

4.16.2 访问者模式的优点

  1. 符合单一职责原则

  2. 优秀的扩展性

  3. 灵活性非常高

4.16.3 访问者模式的缺点

  1. 具体元素对访问者公布细节

  2. 具体元素变更比较困难

  3. 违背了依赖倒置原则

4.16.4 访问者模式的使用场景

  1. 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作,也就是说用迭代器模式已经不能胜任的情景。

  2. 需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。

4.17 适配器模式

4.17.1 适配器模式的定义

将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

4.17.2 适配器模式的优点

  1. 适配器模式可以让两个没有任何关系的类在一起运行,只要适配器这个角色搞定他们就成。

  2. 增加了类的透明性。

  3. 提高了类的复用度。

  4. 灵活性非常好。

4.17.3 适配器模式的使用场景

当你有动机修改已经投产的接口时,适配器模式可能是最适合你的模式。

4.18 组合模式

4.18.1 组合模式的定义

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

组合模式的类图如下图所示

组合模式类图

4.18.2 组合模式的的优点

  1. 高层模块调用简单

  2. 节点自由增加

4.18.3 组合模式的使用场景

  1. 维护和展示部分——整体关系的场景

  2. 从能够一个整体中能够独立出部分模块和功能的场景

4.19 代理模式

4.19.1 代理模式的定义

为其他对象提供一种代理以控制对整个对象的控制。

代理何时的类图如下图所示

代理模式类图

4.19.2 代理模式的的优点

  1. 职责清晰

  2. 高扩展性

  3. 智能化

4.19.3 代理模式的使用场景

AOP

4.20 桥梁模式

4.20.1 桥梁模式的定义

将抽象和实现解耦,使得两者可以独立地变化

桥梁模式的类图如下图所示

桥梁模式类图

4.20.2 桥梁模式的优点

  1. 抽象和实现分类

  2. 优秀的扩展能力

  3. 实现细节对客户透明

4.20.3 桥梁模式的使用场景

  1. 不希望或不适用使用继成的场景

  2. 接口或抽象类不稳定的场景

  3. 重用性要求较高的场景

4.21 装饰模式

4.21.1 装饰模式的定义

动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。

装饰模式的类图如下图所示

装饰模式类图

4.21.2 装饰模式的优点

  1. 装饰类和被装饰类可以独立发展,而不会相互耦合

  2. 装饰模式是继承关系的一个替代方案

  3. 装饰模式可以动态地扩展一个实现类的功能

4.21.3 装饰模式的缺点

多层的装饰是复杂的,尽量减少装饰类的数量,以降低系统的复杂度。

4.21.1 装饰模式的使用场景

  1. 需要扩展一个类的功能,或给一个类增加附加功能。

  2. 需要动态地给一个对象增加功能,这些功能可以再动态地撤销。

  3. 需要为一批的兄弟类进行过改装或加装功能,当然是首选装饰模式。

4.22 门面模式

4.22.1 门面模式的定义

要求一个子系统的外部与其内部的通信必须一个统一的对象进行。

4.22.2 门面模式的优点

  1. 减少系统的相互依赖

  2. 提高了灵活性

  3. 提高了安全性

4.22.3 门面模式的缺点

门面模式最大的缺点就是不符合开闭原则

4.22.4 门面模式的使用场景

  1. 为一个复杂的子系统或模块提供一个供外界访问的接口。

  2. 子系统相对独立——外界对子系统的访问只要黑箱操作即可。

  3. 预防低水平人员带来的风险扩散。

4.22.5 门面模式的注意事项

  1. 一个子系统可以有多个门面

  2. 门面不参与子系统内的业务逻辑

4.23 享元模式

4.23.1 享元模式的定义

使用共享对象可有效地支持大量的细粒度的对象。

4.23.2 享元模式的优点和缺点

  1. 可以大大减少应用程序创建的对象,降低程序内存的占用,增强系统的性能

  2. 提高了系统复杂性感,需要分离出外部状态和内部状态,而且外部状态具有固化特性,不应该随内部状态改变而改变,否则导致系统的逻辑混乱。

4.23.3 享元模式的使用场景

  1. 系统中存在大量的相似对象

  2. 细粒度的对象都具备接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份

  3. 需要缓冲池的场景

4.23.4 享元模式的扩展

  1. 线程安全的问题

  2. 性能平衡的问题

发布于: 2020 年 06 月 18 日 阅读数: 23
用户头像

王海

关注

还未添加个人签名 2018.06.17 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营学习总结——面向对象的设计模式【第三周】