常见设计模式简介

2017-09-09
9 min read

参考网站:runoob

基本原则

graph TB
	A[SO<em>L</em>ID]
	
	A --> B[SRP]
	A --> C[OCP]
	A --> D[LSP]
	A --> E[ISP]
	A --> F[DIP]
	A --> G[LKP]

TIPs

  • 很多设计模式的定义已经很古老了,使用新的技术或者方法实现相同的功能,可能与设计模式最原始的定义并不能完美的匹配,但其中所蕴含的思想其实是相同的
  • 很多设计模式不好理解,不过追溯其起源和实际的应用可以获得实际的例子,有了例子会好理解一些

创建型(4 类)

graph TB
	A[创建型]
	
	B[工厂方法<br/><简单工厂><br/>抽象工厂]
	C[单例]
	D[建造者]
	E[原型]
	
	A --> B
	A --> C
	A --> D
	A --> E

工厂方法( Factory)

  • 定义:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行
  • 简单解释:同类工厂,同类产品,由工厂创建的产品都有相同的接口

Hibernate 为例,Hibernate 可以使用不同类型的数据库(Oracle、MySQL等),通过修改配置文件既可修改 Hibernate 所使用的数据库。通过反射机制 Hibernate 在实现数据库相互替换时满足了 OCP 原则,新数据库只要增加新的数据库对象既可。Hibernate 中数据库对象都实现了相同的接口,即都是同类产品

对于 C++ 这类语言而言,其没有运行时反射机制(但可以使用类似于 <dlfcn.h> 中的方法实现动态方法替换),若想实现工厂方法模式并满足 OCP 原则,只能同时抽象工厂类和产品类,即不同的产品由不同的工厂实例创建,或者使用简单工厂,使代码部分满足 OCP

简单工厂

简单来说简单工厂就是: 一个工厂类,一个产品抽象类。新增产品需要增加产品类并修改工厂类,所以简单工厂不满足 OCP

// 下面的 Shape 对象都继承了相同的接口
// 添加新的形状,例如 Triangle 时需要修改简单工厂函数 getShape,违背了 OCP 原则
public class ShapeFactory{
    public static Shape getShape(ShapeType type){
        switch(type){
            case CIRCLE:
                return new Circle();
            case RECTANGLE:
                return new Rectangle();
            case SQUARE:
                return new Square();
            default:
                throw new UnknownTypeException();
        }
    }
}

抽象工厂

据说抽象工厂最早的应用是在不同的操作系统下创建视窗系统。windows 系统和 linux/unix 系统或者其他种类的系统中都有按钮、文本输入框等 GUI 组件(或者称为控件),不同操作系统界面元素(接口)相同但实现不同,故开发人员抽象出了组件工厂(用于创建不同的控件)和组件工厂的工厂,不同系统下使用不同的组件工厂,具体的组件由组件工厂创建。简单来说这就是一个二级工厂的故事,由一级工厂创建不同操作系统下的组件工厂,具体组件由二级工厂创建

当代的抽象工厂( Abstract Factory Pattern )比其原始定义与实现要复杂一些,但核心思想不变

  • 定义: 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类
  • 产品族的概念,同一个二级工厂生产的产品属于同一个产品族

注意工厂方法和抽象工厂的区别,前者的工厂与产品都只有一种抽象,而后者工厂和产品都有多种抽象

单例模式( Singleton)

  • 定义: 保证一个类仅有一个实例,并提供一个访问它的全局访问点
  • 懒汉式,使用时才创建对象
    • 线程安全(DCL 其实也不一定安全,C++ 中最好使用 call_once
    • 线程不安全
  • 饿汉式,系统启动时创建对象

建造者模式( Builder)

  • 定义:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示
    • 创建一个复杂对象时可能需要分步构建
  • 角色
    • 最终产品(Product)
    • 抽象建造者(Builder),包含构建一个对象的所有构建过程,也可以是一个接口
    • 具体建造者(Concrete Builder),Builder 的一个具体实现
    • 指挥者(Director),使用具体建造者创建对象的对象,这个可以没有

举个不太合适的例子,Java web 中有过滤器或者拦截器的概念

原型模式( Prototype)

  • 定义: 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
  • 对于 Java 这类语言而言对象的拷贝默认是浅拷贝,所以需要实现 clone() 方法以实现深拷贝
  • 对于 C++ 这类语言而言,如果只考虑左值且未自定义拷贝构造,默认就是深拷贝

结构型(7 类)

graph TB
	A[结构型模式]
	
	B[适配器<br/>vs<br/>桥接]
	C[过滤器]
	D[组合]
	E[装饰器]
	F[外观]
	G[享元]
	H[代理]
	
	A --> B
	A --> C
	A --> D
	A --> E
	A --> F
	A --> G
	A --> H

适配器模式( Adapter)

  • 定义:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作

桥接模式(bridge)

  • 定义: 将抽象部分与实现部分分离,使它们都可以独立的变化
    • 注意这里的抽象指的不是接口或者继承,而是功能上的抽象。举个例子,你有一个对象需要持久化数据,但你不知道这个数据要保存在哪里,所以你预留了一个接口(或者存储对象接口),这个接口就是“一座桥”,用于连接当前对象和实际的存储实体
  • 全文检索系统(例如 Lucene)需要分词器,用来将文章分解为单词,不同语言有不同的分词器,Lucene 要预先在系统中定义一个抽象的分词器接口,新增分词器时需要继承这个接口,这个接口就是链接 Lucene 和分词器的桥梁。如果已有的分词系统和 Lucene 不兼容,则你需要对这个分词系统进行封装,这个封装的过程所使用的模式为适配器模式

桥接模式和适配器模式很像,后者是在已有的系统间创建一个中间人来实现系统之间的交互,而桥接模式是预先知道变化并为变化预留接口,这个接口就是 bridge

过滤器模式( Filter)

  • 定义: 使用不同的标准来过滤一组对象,通过逻辑运算以解耦的方式把它们连接起来

对于 Java 程序员而言过滤器模式很常见,例如 Java web 中的 Filter、Struts 和 SpringMVC 中的拦截器等,这些特性所使用的就是过滤器模式

组合模式(Composite)

  • 定义: 将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性

组合模式,就是在一个对象中包含其他对象,这些被包含的对象可能包含其他对象也可能不包含其他对象,一个组的操作将影响到整个组的所有对象

以程序员常用的编辑器为例,在个性化设置中修改编辑器主题的颜色时将一次修改所有控件的颜色。控件一般有继承关系,例如按钮依存与当前 Frame,使用组合模式,在修改父属性的同时也可以递归的修改父空间下所有空间的属性

装饰器模式(Decorator)

  • 定义: 动态地给一个对象添加一些额外的职责
  • 装饰器类和被装饰类的父类是相同的,装饰之后得到一个和被装饰对象类型相同的新对象

装饰器模式可以给已有的功能增加额外的能力,以 Spring AOP 为例,在为函数 f 添加 AOP 切面之后系统在调用 f 时会先触发 AOP 函数的调用,这相当于为 f 增添了新功能

对于 C++ 这类静态语言而言,装饰器是同时继承与包含。只继承则只创建了一个新类,将装饰器类与已有对象关联才是装饰的过程

以一个 RESTful-API 接口为例,假设一个 REST 接口可以通过用户 ID 返回用户手机号

// 返回用户信息的接口
class QueryUserInfos
{
    public:
    	string get_userinfos(string user_id){
            // query user info from database and return 
        }
}

class AuthQueryUserInfos : public QueryUserInfos
{
    public:
        // 关联需要装饰的对象 
    	AuthQueryUserInfos(QueryUserInfos& q):query_userinfos_{q} {}
    	
    	// 为对象已有的行为增加功能,例如权限管理
    	string get_userinfos(string user_id) override
        {
            // Authentication action for user_id or other limitation
            return this->query_userinfos_.get_userinfos(user_id);
        }
    
    private:
    	QueryUserInfos &query_userinfos_;
}

外观模式(Facade )

  • 定义: 为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
  • 例如常见的 MVC 框架结构,每个子系统都有一个确定的界面与外界交互

享元模式( Flyweight )

  • 定义: 运用共享技术有效地支持大量细粒度的对象 ,简单来说就是共享对象
  • 示例:
    • Java 常量内存池中的常用字符串
    • 数据库连接的池化
    • 如果把线程也作为一个对象看待的话,线程池也是一种享元模式

代理(Proxy)

  • 定义: 为其他对象提供一种代理以控制对这个对象的访问
  • 示例:
    • 访问配置中心的本地代理,有代理管理配置中心的连接请求与缓存
    • Spring AOP 的实现基于代理,这个代理会关联实际的执行对象或者函数

行为型(8 类)

graph TB
	A[行为型]
	
	B[责任链]
	C[命令]
	D[解释器]
	E[迭代器]
	F[中介者]
	G[备忘录]
	H[观察者]
	I[状态]
	
	A --> B
	A --> C
	A --> D
	A --> E
	A --> F
	A --> G
	A --> H
	A --> I

责任链

  • 定义: Chain of Responsibility Pattern,避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止

  • 例如 JSP 中的 Filter 链

命令模式(Command)

  • 定义: 请求以命令的形式包裹在对象中,并传给调用对象
  • 在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,需要对“行为请求者”和“行为执行者”进行解耦
  • 示例:
    • 文本编辑器中有撤销与重做操作,编辑器一般使用命令模式实现这种功能
      • 我们对文档的操作,如修改与删除都对应着一个对文档控制器的命令,记录这些命令可以追溯文档的修改历史
    • 数据库中的事务处理也有命令模式的影子

解释器(Interpreter)

  • 定义: 给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子
  • 示例:
    • 规则引擎
    • SQL 解析器、符号处理引擎等

迭代器模式(Iterator )

  • 定义: 提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示

无论是在 C++ 还是 Java 等语言中,迭代器模式都非常常见,例如 C++ 中的大部分容器,我们可以使用 itr 访问所有元素,很多时候这些元素的底层存储机制不同;在 Java 中很多容器提供了 hasNext、next,用来访问元素

中介者模式

  • 定义: 用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互

MVC 框架中 C 就是 M 和 V 的中介者

备忘录模式

  • 定义: 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态
  • 例如
    • 游戏存档、数据库的事务管理等等

观察者模式

  • 定义: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新

比如 redis 中的通知,微信中订阅好的推送,这些都是观察者模式

状态模式

  • 定义: 允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类

J2EE 模式

数据访问对象模式(DAO)

  • 定义:数据访问对象模式(Data Access Object Pattern)或 DAO 模式用于把低级的数据访问 API 或操作从高级的业务服务中分离出来

  • 组成

    1. 模型对象/数值对象(Model Object/Value Object),一个用于保存单个数据库记录的结构体,一般包含成员变量的 getter/setter方法,例如 User 对象就与数据库中用户记录是一一对应的
    2. 数据访问对象接口(Data Access Object Interface),在 Java 中这是一个接口,在 C++ 中这是一个抽象对象,用于描述接口的功能。有时候信息不一定都源自数据库,也可能源自 xml,或者其他介质
    3. 数据访问对象实体类(Data Access Object concrete class),上面接口或者抽象对象的具体实现