17 November 2014

{ What }

每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。(你遇到的问题都是过去曾被别人遇到过的问题,而这些问题通常已有一些通用的解决方案)

{ Why }

设计模式可以确保系统能以特定方式变化,从而帮助你避免重新设计系统。每一个设计模式允许系统结构的某个方面的变化独立于其他方面,这样产生的系统对于某一种特殊变化将更健壮。

{ How }

设计中的抽象对于产生灵活的设计是至关重要的。

在实际的生产实践中发现需求中的变化所在,对变化的地方依照设计原则进行重新设计(包括但不限于:抽象、封装、继承,组合等方式)。如将变化封装到独立的类中,通过继承和多态实现变化的隔离。

{ 名词解释 }

  • 客户端:因为我们所关注的和频繁操纵的是直接面向客户端的代码或者说与客户端有交互的代码,因此,模式旨在从客户端的角度出发进行抽象和封装。
  • 接口:模式中的接口不单是指接口的定义,如:IInterface,而是表示类或者接口暴露给外界的可供外界调用的属性、方法等。
  • 抽象:从外部看一个事物整体
  • 封装:从内部看一个事物构成

{ 设计原则 }

  • 单一职责原则(SRP):就一个类而言,应该仅有一个引起它变化的原因。
  • 开放-封闭原则(OCP):软件实体(类、模块、函数等等)应该可以扩展,但是不可修改。(面对需求,对程序的改动是通过增加新代码进行的,而不是更改现有的代码)
  • 里氏代换原则(LSP):子类型必须能够替换掉它们的父类型。
  • 接口隔离原则(ISP):使用多个专门的接口比使用单一的总接口要好。
  • 依赖倒转原则(DIP)
  • 高层模块不应该依赖低层模块,两者都应该依赖抽象
  • 抽象不应该依赖细节,细节应该依赖抽象
  • 迪米特法则(LoD)或最少知识原则:如果两个类不必要彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三方转发这个调用。如:装饰模式和中介者模式。
  • 组合/聚合复用原则(CARP):尽量使用组合/聚合,尽量不用使用继承。
  • 高内聚、低耦合:模块内高内聚,模块间低耦合。

单例模式

{ 保证唯一实例 }

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式是设计模式中最简单也是最为人所知的模式。单个实例的情况很多,比如,我们常用的 Control PanelTask Manager 都是同时只能有一个实例。单例中要注意的情况是多线程的环境下是否会导致创建多个实例。 Singleton
Implementing the Singleton Pattern in C#
Write a Singleton in Swift

组合模式

{ 部分-整体 }

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

组合通过用于视图构造,树形结构构造的场景,如 iOS 中单个视图可以由多个子视图组成,而其子视图由可以由单个或多个父视图组成,从而相互组合成整个视图树。Android 中子视图与容器视图的关系,但这里存在一个安全性和透明性之间的取舍,由于子视图不存在 addremove 的操作。表达式树也是一种组合模式的应用。使用组合模式,我们可以统一对待单个对象与组合对象,忽略它们之间的差异,从而达到一致性的访问。

职责链模式

{ 分离职责 }

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

职责链的好处就是避免在某一个地方使用多个if/else对请求进行不同的处理,降低耦合度,同时还可以自定义请求处理的顺序。iOS 以及 Android 中视图响应链的传递就是职责链的方式,请求被不断转发给后继者,直至被处理。

代理模式

{ 访问控制 }

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

提供一个代理,该代理与原有的对象有相同的实现,该代理作为客户端与原对象之间的中间人,从而实现客户端对原始对象的访问进行控制,以及控制原有对象对外界暴露出的接口。

适配器模式

{ 接口兼容 }

将一个类的接口转换成客户端希望的另一个接口;使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

由于现有接口与客户端希望的接口不兼容,所以先定义一个满足客户端需要的接口,然后用该接口对现有接口进行包装,从而满足客户端的需求。

外观模式

{ 降低耦合 }

为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

通过增加一个Facade,降低客户端与子系统之间的耦合度,由原先的客户端直接与多个子系统中的接口交互变成只与Facade进行交互,而Facade与其余的子系统进行交互。

策略模式

{ 封装算法 }

定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的用户。

比如:迅雷下载,用户只需要输入链接地址,程序会根据地址的类型不同采用不同的协议算法进行下载。此模式将具体的算法实现与Context解耦,只包含对一个策略接口的引用,而不用管具体的算法细节,方便扩展,增加新的算法。 增加Context的主要目的是为了在访问策略的前后进行更复杂业务上的操作。 stackoverflow

简单工厂+工厂方法

{ 创建简单对象 }

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

简单工厂和工厂方法都是解决创建对象的问题,简单工厂是通过 switch 语句来选择要创建的对象,而工厂方法则提取出一个创建对象的共有接口 IFactory,让每一个具体对象实现这个接口。从而达到对修改封闭,对扩展开放的目的。

用泛型来实现对象的创建,从而代替简单工厂和工厂方法。

抽象工厂

{ 创建对象系列 }

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

抽象工作是当工厂方法涉及更为复杂的多个产品系列衍生而来。比如数据库更换的例子,采用抽象工厂,我们可以依赖于顶层接口,而不再依赖于具体的数据库接口,方便数据库的更换。为了将修改降到最低,通常会 IFactory 及其一系列子接口用单独的类 DataAccess 替换,在类中使用反射手段以最大化降低代码的修改量。

原型模式

{ 创建对象种类,实现不同行为 }

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

原型模式为了应变于对象种类的变化,通过创建不同的种类来实现不同的行为,而不是单单拷贝对象,因此与深浅复制并无关联。

模板方法

{ 方法重写 }

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

我们通过将不可变可变进行分离,将不可变的提取到基类中,可变的在子类中实现。在 .NET 中通过继承和重写可以达到此目的。

装饰模式

{ 扩展功能 }

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

比如为已有的某个类的业务逻辑中添加预处理或者后期处理,不是直接在类中进行更改,而是提供了一种额外供选择的装饰方式(万一又不需要这些处理了呢,或者多个客户端采用不同的处理)。如果想要给一个类增加职责的话,通常我们会采用继承包含两种方式,而继承会导致封装被破坏,所以优先采用包含的方式,装饰模式便是采用包含的方式,对原始对象进行扩展。在 .NET 中,可以使用扩展方法对对象进行扩展。

需要注意的是,装饰类需要与组件类暴露相同的接口(因此不适合组件已有复杂而庞大的接口实现情况,这时可以考虑采用策略替代),将消息转发给组件类,并执行自身的装饰操作。被装饰组件并不需要知道装饰组件。调用方式如下:

window.SetContents(
    new BorderDecorator(
        new ScrollDecorator(textView)
    )
);

状态模式

{ 动态改变状态 }

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

状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况,把状态的判断逻辑转移到表示不同状态的一系列类当中,在 Context 执行状态切换,也可以在 State 子类中获取状态切换的操作,并简化复杂的判断逻辑。如果状态判断很简单,那就没有必要用此模式了。比如,图形编辑器维护一个当前 Tool 对象并将请求委托给它,当用户选择一个新工具时,就将这个工具对象换成新的,从而使得图形编辑器的行为相应地发生改变。(如选择画直线还是画矩形所执行的具体实现不同,而直线和矩形可以表示为具体的 Tool 子类)

备忘录模式

{ 备份状态 }

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

备份状态为什么不用深/浅拷贝呢?不想对上层应用暴露了此接口,限制备份的使用;不需要保存状态的全部信息,即部分信息。(如果在Originator中维护一个Memento实例,则无需使用Caretaker)

迭代器模式

{ 迭代序列 }

提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。

一个聚合对象,比如列表,应该提供一种方法来让别人可以访问它的元素,而又不需要暴露它的内部结构,此外,针对不同的需要,可能要以不同的方式遍历这个列表。但是即使可以预见到所需的那些遍历操作,你可能也不希望列表的接口中充斥着各种不同遍历的操作。有时还可能需要在同一个列表上同时进行多个遍历。

迭代器模式可以帮助我们解决以上问题,这一模式的关键思想是将对于列表的访问和遍历从列表对象中分离出来并放入一个迭代器对象中。 迭代器通常配合工厂方法(Factory Method)一起使用,为在列表中获取迭代器提供一个统一的接口,客户端无需关注具体的迭代器类,通过如 createIterator() 方法,我们可以自由的替换迭代器的实现。

迭代器可以通过定义在聚合类中的嵌套类,或者通过迭代器的构造函数传递聚合对象的方式,使迭代器访问聚合对象中子元素。迭代器模式在 C# 中的实现是使用foreach 配合 IEnumerableIEnumerator 接口实现;迭代器在 Swift 中使用 for 配合 SequenceIteratorProtocol 协议实现。

建造者模式

{ 分离构建与表示 }

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

如果只是构造一个简单的对象,我们可以使用工厂方法。当构造一个复杂的对象时,往往我们会分步骤一步一步地进行构建,Director指挥着构建步骤的顺序,Builder则定义了要实现步骤的抽象接口。抽象对象构造的步骤,使得这些步骤的不同实现能构造出不同对象的表示。

var builder = new ConcreteBuilder();
var director = new Director(builder);
director.Construct(); // builder build part 1, 2, 3
var result = builder.GetResult()

StringBuilderBuilderstackoverflow
Builder的另一种理解方式:stackoverflow


桥接模式

{ 分离变化 }

将抽象部分与它的实现部分分离,使得它们都可以独立地变化。

当我们在继承体系中出现继承者和被继承者都会独自地变化时,就应该考虑使用桥接模式来分离变化,将抽象部分和实现部分的变化各自地独立开,两者之间使用组合/聚合的方式进行通信。或者说,实现某一系统可以从多种角度进行分类,实现继承,而这种继承结构会导致高度的耦合,不宜扩展。此时,应该考虑应用组合/聚合的方式分离变化。

比如:手机品牌和手机软件的例子,两部分都可以独自地变化,并使用组合的方式进行通信,使得增加手机品牌和手机软件都很灵活。

命令模式

{ 封装请求 }

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。

有时必须向某对象提交请求,但并不知道关于被请求的操作或请求的接受者的任何信息。比如在界面设计的菜单栏中,每个菜单都对应着一个操作,而且不同菜单可能执行相同的操作,因此菜单不应该与操作之间保持紧耦合,这时我们可以通过抽象出 Command 抽象基类,将不同的具体操作设计为一个对应的 ConcreteCommand 只需在构造菜单时将 ConcreteCommand 传递给它,在菜单被选中时执行命令操作即可,而无需关注具体操作细节。还可以通过记录操作记录列表提供 undo/redo 等功能。

观察者模式

{ 状态通知 }

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

有时候我们可能希望当某一个操作或者某个对象的状态发生变化时,能够被通知到,甚至有时候我们希望通知到多个对象,而又不会导致观察者和被观察者之间的紧耦合,这时候我们可以采用观察者模式,在 Subject 中维护一系列 Observer 对象,当某个状态发生改变后,依次通知到 Observer 对象的 update 方法。在 C# 中可以采用基础构造 event 来实现观察者模式中通知机制,在 Objective-C 中可以使用 KVO 实现观察者模式(编译器会自动为需要监测的属性插入通知前后的代码调用)。


source

Desgin Pattern

Examples of GoF Design Patterns

Solidify Your C# Application Architecture with Design Patterns