前段时间,在自己糊里糊涂地写了一年多的代码之后,接手了一坨一个同事的代码。身边很多人包括我自己都在痛骂那些乱糟糟毫无设计可言的代码,我不禁开始深思:自己真的比他高明很多吗?我可以很自信地承认,在代码风格和单元测试上可以完胜,可是设计模式呢?自己平时开始一个project的时候有认真考虑过设计模式吗?答案是并没有,我甚至都数不出有哪些设计模式。于是,我就拿起了这本设计模式黑皮书。
中文版《设计模式:可复用面向对象软件的基础》,译自英文版《Design Patterns: Elements of Reusable Object-Oriented Software》。原书由Erich Gamma, Richard Helm, Ralph Johnson 和John Vlissides合著。这几位作者常被称为“Gang of Four”,即GoF。该书列举了23种主要的设计模式,因此,其他地方经常提到的23种GoF设计模式,就是指本书中提到的这23种设计模式。
把书看完很容易,但是要理解透彻,融汇贯通很难,能够在实际中灵活地选择合适的设计模式运用起来就更是难上加难了。所以,我打算按照本书的组织结构(把23种设计模式分成三大类)写三篇读书笔记,一来自我总结,二来备忘供以后自己翻阅。与此同时,如果能让读者有一定的收获就更棒了。我觉得本书的前言有句话很对,“第一次阅读此书时你可能不会完全理解它,但不必着急,我们在起初编写这本书时也没有完全理解它们!请记住,这不是一本读完一遍就可以束之高阁的书。我们希望你在软件设计过程中反复参阅此书,以获取设计灵感”。
本节将介绍行为模式,包括职责链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式和访问者模式等十一种模式。行为模式涉及到算法和对象间职责的分配。行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。这些模式刻画了在运行时难以跟踪的复杂的控制流,它们将你的注意力从控制流转移到对象间的联系方式上来。行为类模式使用继承机制在类间分派行为,如模板方法和解释器。行为对象模式则使用对象复合而不是继承,一些行为对象模式描述了一组对等的对象怎样相互协作以完成其中任一对象都无法单独完成的任务,如职责链、中介者和观察者。其他的行为对象模式常将行为封装在一个对象中并将请求指派给它,如策略模式、命令模式、状态模式、访问者模式和迭代器模式。
1. 职责链模式(Chain of responsibility)
职责链模式,使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
动机:
考虑一个图形用户界面中的上下文有关的帮助机制。用户在界面的任一部分上点击都可以得到帮助信息,帮助信息依赖于点击的界面上下文,如果该部分没有帮助信息,则显示一个关于当前上下文的比较一般的帮助信息。很显然应该按照从特殊到最普遍的顺序来组织帮助信息,而且提交帮助请求的对象并不明确知道谁是最终提供帮助的对象。职责链模式的想法是,给多个对象处理一个请求的机会,从而解耦合发送者和接受者。该请求沿对象链传递,链中收到请求的对象要么亲自处理它,要么转发给链中的下一个候选者,直至其中一个对象处理它。
UML结构图:
代码示例:
下面是动机那一节中提到的提供帮助信息的简单示例。
public class HelpHandler
{
private HelpHandler successor;
public HelpHandler(HelpHandler helpHandler)
{
this.successor = helpHandler;
}
public virtual void HandleHelp()
{
if(successor != null)
{
successor->HandleHelp();
}
}
}
public class Widget : HelpHandler
{
private Widget parent;
public bool HasHelp()
{
//Check if we have help
}
public Widget(parent) : HelpHandler(parent)
{
}
}
public class Button : Widget
{
public Button(Widget button) : Widget(button)
{
}
public override void HandleHelp()
{
if(this.HasHelp())
{
//offer help on the button
} else {
this.HandleHelp();
}
}
}
适用情况:
(1) 有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定;
(2) 你想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求;
(3) 可处理一个请求的对象结合应被动态指定。
2. 命令模式(Command)
命令模式,将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化。如,对请求进行排队、记录请求日志,以及支持可撤销的操作。
动机:
有时必须向某对象提交请求,但并不知道关于被请求的操作或者请求的接受者的任何消息。例如,用户界面工具箱包括按钮和菜单这样的对象,它们执行请求响应用户输入。但工具箱不能显示地在按钮或菜单中实现该请求,因为只有使用工具箱的应用知道该由哪个对象做哪个操作。而工具箱的设计者无法知道请求的接受者或执行的操作。命令模式通过将请求本身变成一个对象来使工具箱对象可向未指定的应用对象提出请求。这一模式的关键在于抽象的Command类,它定义了一个执行操作的接口,如Execute操作。
UML结构图:
代码示例:
下面是一个简单的Command模式的示例,我们实现了开关的“从开到关”和“从关到开”转换的command类。
public interface ICommand
{
void Execute();
}
public interface ISwitchable
{
void PowerOn();
void PowerOff();
}
public class CloseSwitchCommand : ICommand
{
private ISwitchable switchable;
public CloseSwitchCommand(ISwitchable switchable)
{
this.switchable = switchable;
}
public void Execute()
{
this.switchable.PowerOn();
}
}
public class OpenSwitchCommand : ICommand
{
private ISwitchable switchable;
public OpenSwitchCommand(ISwitchable switchable)
{
this.switchable = switchable;
}
public void Execute()
{
this.switchable.PowerOff();
}
}
public class Switch
{
ICommand closedCommand;
ICommand openedCommand;
public Switch(ICommand closedCommand, ICommand openedCommand)
{
this.closedCommand = closedCommand;
this.openedCommand = openedCommand;
}
public void Close()
{
this.closedCommand.Execute();
}
public void Open()
{
this.openedCommand.Execute();
}
}
适用情况:
(1) 抽象出待执行的动作以参数化某对象;
(2) 在不同的时刻指定、排列和执行请求;
(3) 支持取消操作;
(4) 支持修改日志,这样系统崩溃时,这些修改可以被重做一遍;
(5) 用构建在原语操作上的高层操作构造一个系统。
3. 解释器模式(Interpreter)
解释器模式,给定一个语言,定义它的文法表示和一个解释器,这个解释器使用所定义的文法表示来解释语言中的句子。
动机:
例如,对于搜索匹配一个模式的字符串问题,正则表达式是描述字符串模式的一种标准语言。与其为每一个模式都构造一个特定的算法,不如使用一种通用的搜索算法来解释执行一个正则表达式,该正则表达式定义了待匹配字符串的集合。解释器模式描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解释这些句子。
UML结构图:
代码示例:
下面是一个只包含TerminalExpression和NonterminalExpression的解释器模式的简单框架。
public class Context
{
}
abstract class AbstractExpression
{
public abstract void Interpret(Context context);
}
public class TerminalExpression : AbstractExpression
{
public override void Interpret(Context context)
{
// Interpret for terminal expression
}
}
public class NonterminalExpression : AbstractExpression
{
public override void Interpret(Context context)
{
// Interpret for nonterminal expression
}
}
适用情况:
当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。
(1) 文法简单。对于复杂的文法,文法的类层次变得庞大而无法管理;
(2) 效率不是一个关键问题。最高效的解释器通常不是通过直接解释语法分析树实现的,而是首将它们转换成另一种形式。
4. 迭代器模式(Iterator)
迭代器模式,提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。
动机:
一个聚合对象,如列表,应该提供一种方法来让别人可以访问它的元素,而又不需要暴露它的内部结构。此外,针对不同的需要,可能要以不同的方式遍历这个聚合对象。迭代器模式的关键思想是,将对列表的访问和遍历从列表对象中分离出来并放入一个迭代器对象中。迭代器类定义了一个访问该列表元素的接口,迭代器对象负责跟踪当前的元素。
UML结构图
代码示例:
下面一个简单的例子实现了一个Iterator来访问Item集合。
public class Item
{
private string name;
public Item(string name)
{
this.name = name;
}
public string Name => this.name;
}
public interface IAbstractCollection
{
Iterator CreateIterator();
}
public class Collection : IAbstractCollection
{
private ArrayList items = new ArrayList();
public Iterator CreateIterator()
{
return new Iterator(this);
}
public int Count => items.Count;
public object this[int index]
{
get {return items[index];}
set {items.Add(value);}
}
}
public interface IAbstractIterator
{
Item First();
Item Next();
bool IsDone {get;}
Item CurrentItem {get;}
}
public class Iterator : IAbstractIterator
{
private Collection collection;
private int current = 0;
public Iterator(Collection collection)
{
this.collection = collection;
}
public Item First()
{
current = 0;
return collection[current] as Item;
}
public Item Next()
{
++current;
if(IsDone)
return null;
else
return collection[current] as Item;
}
public Item CurrentItem
{
get {return collection[current] as Item;}
}
public bool IsDone
{
get {return current >= collection.Count;}
}
}
适用情况:
(1) 访问一个聚合对象的内容而无需暴露它的内部表示;
(2) 支持对聚合对象的多种遍历;
(3) 为遍历不同的聚合结构提供一个统一的接口(即支持多态迭代)。
5. 中介者模式(Mediator)
中介者模式,用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显示地相互作用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
动机:
面向对象设计鼓励将行为分布到各个对象中,这种分布可能会导致对象间有许多连接。大量的相互连接使得一个对象似乎不太可能在没有其他对象的支持下工作。可以通过将集体行为封装在一个单独的中介者对象中避免这个问题。中介者负责控制和协调一组对象间的交互,中介者充当一个中介以使组中的对象不再相互显示引用。这些对象仅知道中介者,从而减少了相互连接的数目。
UML结构图:
代码示例:
下面这个简单的中介者的例子,所谓的中介者就是由它来与Component1和Component2来交互,使得它们之间是松耦合的。
public interface IComponent
{
void SetState(object state);
}
public class Component1 : IComponent
{
public void SetState(object state)
{
// Set state for component1
}
}
public class Component2 : IComponent
{
public void SetState(object state)
{
// Set state for component2
}
}
public class Mediator
{
public IComponent Component1 {get; set;}
public IComponent Component2 {get; set;}
public void ChangeState(object state)
{
this.Component1.SetState(state);
this.Component2.SetState(state);
}
}
适用情况:
(1) 一组对象以定义良好但是复杂的方式进行通信,产生的相互依赖关系结构混乱且难以理解;
(2) 一个对象引用其他很多对象并且直接与这些对象通信,导致难以复用该对象;
(3) 想定制一个分布在多个类中的行为,而又不想生成太多的子类。
6. 备忘录模式(Memento)
备忘录模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
动机:
有时有必要记录一个对象的内部状态。为了允许用户取消不确定的操作或从错误中恢复过来,需要实现检查点和取消机制,而实现这些机制,你必须事先将状态信息保存在某处,这样才能将对象恢复到它们先前的状态。但是对象通常封装了其部分或所有的状态信息,使得其状态不能被其他对象访问。备忘录模式就派上用场了,一个备忘录是一个对象,它存储另一个对象在某个瞬间的内部状态,而后者称为备忘录的原发器。当需要设置原发器的检查点时,取消机制会向原发器请求一个备忘录,原发器用扫描当前状态的信息初始化该备忘录。
UML结构图:
代码示例:
下面是一个简单的Memento模式的例子,我们在Originator中有一个Memento对象,用来保存运行过程中Originator的state,这样当它需要回退时可以取回之前保存在Memento中的state。
public class State
{
}
public class Memento
{
private State state;
public Memento(State state)
{
this.state = state;
}
public State GetState()
{
return this.state;
}
public void SetState(State state)
{
this.state = state;
}
}
public class Originator
{
private Memento memento;
private State currentState;
public Originator(State state)
{
this.currentState = state;
this.memento = new Memento(state);
}
public SetMemento(Memento memento)
{
memento.SetMemento(currentState);
}
public void Revert()
{
this.currentState = memento.GetState();
}
}
适用情况:
(1) 必须保存一个对象在某个时刻的(部分)状态,这样以后需要时它才能恢复到先前的状态。
(2) 如果一个用接口来让其他对象直接得到这状态,将会暴露对象的实现细节并破坏对象的封装性。
7. 观察者模式(Observer)
观察者模式,定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
动机:
将一个系统分割成一系列相互协作的类有一个常见的副作用,需要维护相关对象间的一致性。观察者模式描述了如何建立这种相关关系。这一模式的关键对象是目标和观察者,一个目标可有任意数目的依赖它的观察者。一旦目标的状态发生改变,所有的观察者都得到通知。
UML结构图:
代码示例:
下面是一个经典的关于观察者模式的示例。我们有一个Heater用来加热水,当水温达到一定得温度后会触发BoilEvent,而BoilEvent上绑定为委托都会被执行。
public class Heater
{
private int temperature;
public delegate void BoilHandler(int param);
pubilc event BoilHandler BoilEvent;
public void BoilWater()
{
foreach(int t in Enumerable.Range(0, 101))
{
temperature = t;
if(temperature > 95)
{
if(BoilEvent != null)
{
BoilEvent(temperature);
}
}
}
}
}
public class Alarm
{
public void MakeAlert(int param)
{
Console.WriteLine($"Alarm : the temperature of water reached {param}");
}
}
public class Display()
{
public static void ShowMessage(int param)
{
Console.WriteLine($"Display: the temperature of water reached {param}");
}
}
Usage:
Heater heater = new Heater();
heater.BoilEvent += (new Alarm()).MakeAlert;
heater.BoilEvent += Display.ShowMessage;
heater.BoilWater();
适用情况:
(1) 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面。
(2) 当对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象待改变。
(3) 当一个对象必须通知其他对象,而它又不能假定其他对象是谁。
8. 状态模式(State)
状态模式,允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
动机:
考虑一个表示网络连接的类TCPConnection,一个它的对象的状态可以处于Established、Listening或Closed。当一个TCPConnection对象接收到其他对象的请求时,它根据自身的当前状态做出不同的反应。State模式描述了TCPConnection如何在每一种状态下表现出不同的行为。这一模式的关键思想是引入一个称为TCPState的抽象类来表示网络的连接状态,TCPState类为各个表示不同的操作状态的子类声明了一个公共的接口,而它的子类则会去实现与特定状态相关的行为。
UML结构图:
代码示例:
下面是状态模式的简单示例。SateContext类中有一个对IState对象,动态执行时具体的State变化后相应的相应Request的方法也会变化。
public interface IState
{
void Handle()
{
}
}
public class ConcreteStateA : IState
{
public void Handle()
{
// Handle for ConcreteStateA
}
}
public class ConcreteStateB : IState
{
public void Handle()
{
// Handle for ConcreteStateB
}
}
public class StateContext
{
private IState state;
public StateContext(IState state)
{
this.state = state;
}
public void Request()
{
this.state.Handle();
}
}
适用情况:
(1) 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为;
(2) 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。
9. 策略模式(Strategy)
策略模式,定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。本模式使得算法可独立于使用它的客户而变化。
动机:
有许多算法可对一个正文流进行分行,将这些算法硬编码进使用它的类中是不可取的。因为这样做不仅使得客户程序变得复杂难以分割,而且可能还需要支持一些我们并不会使用的换行算法。因此,我们可以定义一些类来封装不同的换行算法,从而避免这些问题。假设一个Composition类负责维护和更新一个正文浏览程序中显示的正文换行。换行策略不是Composition类实现的,而是由抽象的Compositor类的子类(实现不用的换行策略)各自独立地实现的,Composition类只维护一个对Compositor对象的引用。
UML结构图:
代码示例:
下面是一个简单的策略模式示例。我们对value1和value2会根据相应的策略来执行加法或者减法。
public interface ICalculate
{
int Calculate(int value1, int value2);
}
public class Minus : ICalculate
{
public int Calculate(int value1, int value2)
{
return value1 - value2;
}
}
public class Plus : ICalculate
{
public int Calculate(int value1, int value2)
{
return value1 + value2;
}
}
public class CalculateClient
{
private ICalculate strategy;
public int Calculate(int value1, int value2)
{
return strategy.Calculate(value1, value2);
}
public void SetCalculate(ICalculate strategy)
{
this.strategy = strategy;
}
}
适用情况:
(1)许多相关的类仅仅是行为有异。策略提供了一种用多个行为中的一个行为来配置一个类的方法;
(2)需要使用一个算法的不同变体;
(3)算法使用客户不应该知道的数据。这种情况下可使用策略模式以避免暴露复杂的、与算法无关的数据结构。
10. 模板方法模式(Template Method)
模板方法,定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
动机:
考虑一个提供Application和Document类的应用数据,Application类负责打开一个已有的外部形式存储的文档,如一个文件。一旦一个文档中的信息从该文件中读出后,它就由一个Document对象表示。用框架构建的应用可以通过继承Application和Document来满足特定的需求。例如,一个绘图应用定义DrawApplication和DrawDocument子类;一个电子表格应用定义SpreadsheetApplication和SpreadSheetDocument子类。OpenDocument定义了一个打开文档的每一个主要步骤(检查该文档是否打开,创建相应的Document对象,读取等)。我们称OpenDocument为一个模板方法,它用一些抽象的操作定义一个算法,而子类将重定义这些操作提供具体的行为。
UML结构图:
代码示例:
下面是动机这一节中介绍的例子的简单实现。
abstract class Document
{
public abstract void DoRead();
}
abstract class Application
{
public abstract bool CanOpenDocument();
public abstract Document DoCreateDocument();
public void OpenDocument()
{
if(CanOpenDocument())
{
Document document = DoCreateDocument();
document.DoRead();
}
}
}
public class DrawApplication : Application
{
public override bool CanOpenDocument()
{
}
public override Document DoCreateDocument()
{
// Return DrawDocument
}
}
public class SpreadsheetApplication : Application
{
public override bool CanOpenDocument()
{
}
public override Document DoCreateDocument()
{
// Return SpreadsheetDocument
}
}
适用情况:
(1) 一次性实现一个算法的不变部分,并将可变的行为留给子类来实现;
(2) 各个类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复;
(3) 控制子类扩展。
11. 访问者模式(Visitor)
访问者模式,表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
动机:
考虑一个编译器,它将源程序表示为一个抽象语法树。该编译器需在抽象语法树上实施某些操作以进行“静态语义”分析,也需要生成代码。因此它可能要定义许多操作以进行类型检查、代码优化、流程分析,此外,还可使用抽象语法树进行优美格式打印、程序重构等等。这些操作大多要求对不同的节点进行不同的处理。这里有两个问题,一个是,将所有这些操作分散到各种结构点类中会导致整个系统难以理解、难以维护和修改。另一个是,增加新的操作通常需要重新编译所有这些类。因此,我们可以将每个类中相关的操作包装在一个独立的对象(称为Visitor)中,并在遍历抽象语法树时将此对象传递给当前访问的元素。
UML结构图:
代码示例:
下面是一个visitor模式的简单示例。我们用CarElement来接收一个visitor对象,在visitor类中对car的不同组成部分实现不同的visit方法。
public interface CarElementVisitor
{
void visit(Wheel wheel);
void visit(Engine engine);
void visit(Body body);
void visit(Car car);
}
public interface CarElement
{
void accept(CarElementVisitor visitor);
}
public class Wheel : CarElement
{
private string name;
public Wheel(string name)
{
this.name = name;
}
public void accept(CarElementVisitor visitor)
{
visitor.visit(this);
}
}
public class Engine : CarElement
{
public void accept(CarElementVisitor visitor)
{
visitor.visit(this);
}
}
public class Body : CarElement
{
public void accept(CarElementVisitor visitor)
{
visitor.visit(this);
}
}
public class Car : CarElement
{
private List elements;
public Car()
{
this.elements = new List()
{
};
}
public void accept(CarElementVisitor visitor)
{
foreach(CarElement element in elements)
{
element.accept(visitor);
}
visitor.visit(this);
}
}
public class CarElementPrintVisitor : CarElementVisitor
{
public void visit(Wheel wheel)
{
Console.WriteLine("Visiting wheel")
}
public void visit(Engine engine)
{
Console.WriteLine("Visiting engine")
}
public void visit(Body body)
{
Console.WriteLine("Visiting body")
}
public void visit(Car car)
{
Console.WriteLine("Visiting car")
}
}
适用情况:
(1) 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖其具体类的操作;
(2) 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类;
(3) 定义对象结构的类很少改变。但经常需要在此结构上定义新的操作,改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。
参考文献:
《设计模式:可复用面向对象软件的基础》
Chain-of-responsibility pattern
Command pattern
Iterator pattern
Mediator pattern
Memento pattern
Template method pattern
Visitor pattern
C# 中的委托和事件