本文为阅读大话设计模式的读书笔记,并尽可能简短地总结了涉及的设计原则,以一个表格总结了23种设计模式便于记忆。参考文献见文末。
设计模式原则(SOLID CD)
- 开闭原则:对扩展开放,对修改关闭
- 单一职责原则:一个类只有一个发生变化的原因
- 里氏替换原则:引用基类对象的地方能够透明地使用其子类的对象
- 依赖倒置原则:抽象不应依赖于细节,细节应依赖于抽象(针对接口编程)
- 接口隔离原则:类间的依赖关系应该建立在最小的接口上(大接口分成多个小接口)
- 迪米特法则(最少知道原则):对自己依赖的类知道的越少越好,尽少与其他实体相互作用
- 合成复用原则:尽量使用对象组合/聚合,而不是继承关系进行复用
设计模式总结
设计模式详细介绍
创建型模式
0 简单工厂模式 (Simple Factory Pattern)
专门定义一个工厂类来负责创建其他类的实例,可根据创建方法的参数来返回不同实例。
(属于工厂方法模式的特例,不属于GoF23)
- 优点:使用者只需传入一个约定好的参数,就可获取所需的对象,一定程度上减少系统的耦合。
- 缺点:添加新产品需要修改工厂类原有的判断逻辑,违背开闭原则。
1 工厂方法模式 (Factory Method Pattern) (工厂模式)
工厂父类定义创建对象的接口,而工厂子类则负责生成具体的对象。工厂方法使一个类的实例化延迟到其子类
- 优点:当加入新产品时,只要添加一个具体工厂和对应的具体产品,符合开闭原则。
- 缺点:每加一个产品,就需要增加一个产品工厂的类,增加了额外的开发量。
和简单工厂的区别:简单工厂是由一个代工厂生产不同的产品,而工厂方法是对工厂进行抽象化,不同产品都由专门的具体工厂来生产。
简单工厂模式的优点在于工厂类中包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。但问题也就在这里,当增加新产品时,需要修改原有的类中的switch-case,违背开闭原则。
工厂方法模式实现时,客户端需要决定实例化哪一个工厂来实现运算类,选择判断的问题还是存在的,也就是把简单工厂的内部逻辑判断移到了客户端代码来进行。你想要加功能,本来是改工厂类的,而现在是修改客户端。
(利用反射可以解决分支判断的问题)
2 抽象工厂模式 (Abstract Factory Pattern)
提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
(一次创建一个产品族)
和工厂模式的区别:以数据库操作为例,如果只有一个操作User表的类(AccessUser,SqlServerUser)那只需要工厂模式,如果还增加了Department表操作的类(AccessDepartment,SqlServerDepartment),那就需要抽象工厂一起创建。
- 优点:易于交换产品系列,由于具体工厂类只在初始化时出现一次,这就使得改变一个应用的具体工厂很容易。
- 优点:让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操纵实例,产品具体类名也不会出现在客户端中。
- 缺点:规定了所有可能被创建的产品集合,产品族中扩展新的产品困难。(例如数据库操作中切换不同的DB很方便,但要增加一张表的访问则需要增加多个类&修改接口)
可以用反射+简单工厂模式改进。只有一个具体工厂类,包含所有的创建函数,创建什么产品均通过字符串反射实现。
所有在用简单工厂的地方,都可以考虑用反射技术来去除switch或if,解除分支判断带来的耦合。
3 建造者模式 (Builder Pattern) (生成器模式)
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
应用:创建一些复杂的对象,这些对象内部构建间的建造顺序通常是稳定的,但对象内部的构建通常面临着复杂的变化。
-
优点:将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
-
优点:可以很方便的替换或增加具体建造者,无需修改原来代码(指挥者类针对抽象建造者类编写)
-
缺点:所创建的产品需要具有较多的共同点,如果产品之间的差异性很大,则不适合使用。
4 单例模式 (Singleton Pattern)
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
使用全局变量不能防止你实例化多个对象。最好的办法就是,让类自身负责保存它的唯一实例,并保证没有其他实例可以被创建。
Singleton类:
- 构造函数设为private,外部无法new
- 预加载:定义静态GetInstance操作访问它的唯一实例,在其中判断实例是否null等并自动new初始化,考虑多线程初始化需要加锁
- 懒加载:静态初始化,在类中private static readonly Singleton instance =new Singleton()
5 原型模式(Prototype Pattern)
用原型实例指定待创建对象的类型,并且通过拷贝这个原型来创建新的对象。(可简化对象的创建过程)
在.Net的System命名空间中有一个ICloneable接口,其中唯一的方法是Clone(),实现这个接口即可完成原型模式。
和拷贝构造函数的区别:原型模式实现的是一个clone接口,能通过基类指针来复制派生类对象,不需要知道类名。拷贝构造函数完不成这样的任务。
结构型模式
6 适配器模式 (Adapter Pattern)
将一个接口转换成客户希望的另一个接口,使得由于接口不兼容而不能一起工作的那些类可以工作。
适用性:想使用一个已经存在的类,但如果它的接口,也就是它的方法和你的要求不相同时。在双方都不太容易修改时使用,一般是设计后期。
7 装饰模式 (Decorator Pattern)
动态地给一个对象增加一些额外的功能。就增加功能来说,装饰模式比生成子类更为灵活。
- 优点:把类的核心职责和装饰功能区分开。
- 优点:符合开闭原则,可以简化原有的类。
- 缺点:需要创建一些具体装饰类,会增加系统的复杂度。
8 代理模式 (Proxy Pattern)
为其他对象提供一种代理,以控制对这个对象的访问。
应用:调用时做一些额外的事情例如引用计数;根据需要创建开销很大的对象;控制真实对象访问时的权限。
和装饰器模式的区别:装饰器模式是为了增强功能,而代理模式是为了加以控制。
- 优点:可以降低耦合度;或在访问对象时引入一定的间接性,以附加多种用途
- 缺点:在客户端和被代理对象之间增加了代理对象,可能会降低性能。
9 外观模式 (Facade Pattern)
定义了一个高层接口,为子系统中的一组接口提供一个统一的接口,使得这一子系统更加容易使用。
应用:例如在三层架构中,要在层与层之间建立外观 Facade ,这样可以为复杂的子系统提供一个简单的接口,使得耦合度大大降低。
- 优点:实现了客户端与子系统间的解耦,使得调用过程更容易,且子系统便于扩展维护
- 优点:符合迪米特法则(最少知道原则),子系统只需要将需要外部调用的接口暴露给外观类即可。
- 缺点:违背了开闭原则,在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的代码。
10 桥接模式 (Bridge Pattern)
将抽象部分与它的实现部分分离,使它们都可以独立地变化。(优先使用组合/聚合,而不是类继承)
实现系统可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合。
举例:不要从手机类下派生插移动卡的手机、插联通卡的手机,而是抽离出手机卡类,然后通过组合使用。
- 优点:将复杂的类进行分割,优先使用对象组合,符合开闭原则和合成复用原则。
- 缺点:在设计之前,需要识别出两个独立变化的维度。
11 组合模式 (Composite Pattern)
将对象组合成树形结构以表示‘部分-整体’的层次结构。使得对单个对象和组合对象的使用具有一致性。
透明模式:Component中声明统一的接口,Leaf也有无意义的Add/Remove方法,但是方便调用。
安全模式:Component中不声明Add/Remove方法,而在Composite中声明;客户端调用时需要判断。
- 优点:使得客户端代码可以一致地处理单个对象和组合对象,简化了代码
- 优点:更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”
- 缺点:设计较复杂,客户端需要花更多时间理清类之间的层次关系
- 缺点:不容易用继承的方法来增加构件的新功能
12 享元模式 (Flyweight Pattern)
运用共享技术复用大量细粒度的对象。减少内存占用,提升性能。
应用:相同内容的string字面量占同一份内存,围棋/五子棋游戏中的棋子基本属性共用。
-
优点:减少内存中对象的数量,提升性能。
-
缺点:需要分离出内部状态和外部状态,将不能共享的状态外部化,这使得程序的逻辑复杂化。
行为型模式
13 策略模式 (Strategy Pattern)
定义了算法家族,分别封装起来,并让它们可以相互替换。让算法的变化不会影响到使用它的客户。
- 优点:符合开闭原则。策略类的等级结构定义了一个算法族,恰当使用继承可以把公共的代码移到抽象类中以便复用。
- 优点:简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。
- 缺点:系统会产生很多具体策略类。
- 缺点:选择具体实现的职责从客户端转移到Context对象,但不能完全消除if-else(解决方案:使用反射)。
14 模板方法模式 (Template Method Pattern)
定义一个操作中算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
举例:抽象类中的TemplateMethod定义了各种Operations执行的顺序,子类只要继承并实现各个Operations即可。
还可在父类中定义钩子方法,通过子类实现钩子方法对父类的执行进行约束。
- 优点:把不变行为搬移到超类,去除子类中的重复代码。符合单一职责原则和开闭原则。
- 缺点:要为每个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多会导致类的个数增加,此时可结合桥接模式。
15 观察者模式 (Observer Pattern) (发布-订阅模式,模型-视图模式)
定义一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象;其在状态变化时会通知所有观察者对象,使它们能够自动更新自己。
- 优点:可实现表示层和数据逻辑层的分离(两者不需要紧密耦合)
- 优点:在观察目标和观察者之间建立一个抽象的耦合(观察者只需实现Update接口,符合依赖倒转原则)
- 缺点:如果观察者很多,通知很费时;如果观察对象之间出现循环依赖会出问题
扩展:如果遇到事先写好的控件无法更改,则需要利用事件委托机制。
16 中介者模式 (Mediator Pattern)
用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
将网状的系统结构变成一个以中介者对象为中心的星形结构。
应用:在.Net应用程序中的Form窗体,各个控件之间的交互都是由Form来完成(通过事件机制,事件的执行在Form窗体代码中)。MVC中的C。
- 优点:简化对象交互,容易理解扩展;将同事对象解耦
- 缺点:具体中介者类中包含了大量同事之间的交互细节,可能会导致其很复杂,难以维护
17 职责链模式 (Chain of Responsibility Pattern)
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿该链传递请求,直到有对象处理它为止。
- 优点:请求者不用管哪个对象来处理,反正该请求会被处理。
- 优点:职责链中对象仅需维持一个指向其后继者的引用,而不需要知道所有的后继者,可简化连接并减少耦合度。
- 缺点:配置不当时可能请求到了末端也没被处理;链条长可能会有性能问题。
18 命令模式 (Command Pattern) (动作模式,事务模式)
将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化;对请求排队或者记录日志,以及支持可撤销的操作。
- 优点:把请求一个操作的对象与知道怎么执行操作的对象分隔开(请求者与接收者之间解耦)
- 优点:容易设计命令队列,可以记录命令日志,可以实现撤销重做
- 缺点:可能会导致某些系统有过多的具体命令类
19 备忘录模式 (Memento Pattern) (快照模式)
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样可以在以后恢复对象。
与Clone对比:Clone对象会对上层应用开放全部(public)接口,这对于保存备份有时候是不合适的(备忘录的内容通常不能被操作和查看)。
- 优点:提供了一种状态恢复的实现机制,并实现了对信息的封装;结合列表/堆栈存储可以实现撤销等操作。
- 缺点:资源消耗过大,如果储存对象的成员变量太多,会占用大量的存储空间。
20 状态模式 (State Pattern)
允许一个对象在内部状态改变时改变其行为,这个对象看起来像是改变了其类。
将一个对象的状态从该对象中分离出来,封装到专门的状态类中。
应用:解决复杂对象的状态转换&不同状态下行为的封装问题。
- 优点:封装了状态转换规则,消除庞大的条件分支语句,把状态转移逻辑分布到状态子类之间
- 优点:将状态相关的行为放入一个对象中
- 缺点:增加系统中类和对象的个数,增加运行开销
- 缺点:对开闭原则的支持不太好,增加新的状态类需要修改那些负责状态转换的源代码
21 迭代器模式 (Iterator Pattern)
提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。
作用:为遍历不同的聚集结构提供如开始、下一个、是否结束、当前哪一项等统一的接口。
现在高级编程语言如C#、Java等本身己经把这个模式做在语言中了。如C#中的foreach in就是利用IEnumerator和IEnumerable接口迭代。
22 解释器模式 (Interpreter Pattern)
定义一个语言的文法,并建立一个解释器来解释该语言中的句子。
应用:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。
举例:正则表达式。自定义的迷你语言。
-
优点:易于改变和扩展文法。由于使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
-
缺点:为文法中的每一条规则至少定义了一个类,因此包含许多规则的文法可能难以管理和维护。建议当文法非常复杂时,使用其他的技术如语法分析程序或编译器生成器来处理。
23 访问者模式 (Visitor Pattern)
表示一个作用于某对象结构中的各元素的操作。使得可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
目的:将数据结构与数据操作解耦(适用于数据结构稳定的系统)
- 优点:增加新的访问操作很方便,只需增加一个新的访问者(否则需要给每个具体元素增加类方法)。
- 优点:实现双分派机制,通过在ConcreteElement.Accept中传递this来调用visit(多态+重载时使用动态绑定)。
- 缺点:增加新的数据结构变得困难。