文章

设计模式

设计模式

本文是阅读《(GoF:设计模式——可复用面向对象软件的基础》一书的笔记,对书中阐述的所有设计模式进行了总结,并额外参考了一些其他资料,结合了笔者的思考。

概述

设计模式是为了创建可复用的面向对象的软件而出现的。可复用有这么重要吗?答案是有的。可复用的软件有两大好处:一是方便快速构建类似的软件,二是能更好地适应变化。

对于前者,软件的可复用性一旦提高,就可以形成多个可复用的模块和框架,避免写大量重复的代码,以及大量地手动复制代码片段。对于后者,由于软件的需求随时可能发生变化,提前预判这些变化或者说为变化做准备非常有价值,而复用性决定了为了适应这些变化需要修改代码的量,因为复用性高则往往意味着模块化好,即模块高内聚、低耦合,自然地,应对变化也就更容易。

设计模式的核心要点如下:

  • 对接口编程,而非对实现编程。即使用时,不要使用具体的类,而是使用抽象类,抽象类中定义了必要的接口,子类必须实现这些接口,使用者不关心子类,只关心这些接口。例如 Abstract Factory 等。
  • 把可复用的部分抽象为类,哪怕它看起来不必要,或者不符合一般直觉。例如 Factory Method 就是把创建一个类这一行为提取出来,放在一个单独的类中。Facade 也类似,将常用的子系统的 API 提取出来,放在一个 Facade 类中。
  • 复用时优先使用组合而非继承。组合方式具有更好的封装性,只有组合方式无法使用时才应考虑继承。委托是一种组合方法,它使组合具有与继承同样的复用能力。

创建型设计模式

  • Factory Method:将创建对象这一行为封装起来,返回的是抽象类的指针或引用。
  • Abstract Factory:针对不同的产品系列有同一个抽象类,不同产品系列都有同一套产品,该抽象类中定义了这些产品的创建接口。举例来说,不同操作系统下有不同的 GUI 实现,比如 Windows 下的 .net 框架,Linux 下的 X11 和 wayland 等。但不同的 GUI 实现都有类似的组件,如 Button、Label 等。
  • Builder:多次调用其中的 buildPart 函数,直到构建出整体。
  • Prototype:通过克隆原型来创建新的对象。
  • Singleton:对于某个类,只允许创建一个实例对象,程序中可能随时引用此实例对象。常见的应用如配置、日志等。这应该是我最早接触到的设计模式。

    关于单例模式,网上有大量深入的探讨,书中提到的实现方式存在两个主要的问题:指针未释放可能导致内存泄漏、多线程环境下可能出错。最终大家比较推荐的是 Meyers 提出的局部 static 的方式(在《Scott Meyers:Effective C++——55 Specific Ways to Improve Your Programs and Designs》中提出,但不是很明确地指出):

    1
    2
    3
    4
    5
    
    static Singleton& instance()
    {
         static Singleton s;
         return s;
    }
    

    相关链接:

结构型设计模式

  • Adapter:两个功能类似但接口不同的库可以使用该设计模式进行桥接。比如我有一个写好的程序,其中设计了某个驱动接口,但某个厂商提供的驱动接口不符合此接口,就可使用此设计模式,创建一个子类继承自我设计的驱动接口,然后在其中创建一个厂商提供的类的实例(对象),调用它的接口去实现我想要的接口。如果调用它的接口无法实现我想要的接口呢?那么这时可以考虑多继承而非上述的组合方式,多继承即让子类同时继承自我设计的驱动接口和厂商提供的驱动接口,从而可以访问其中的私有成员等实现我想要的接口。理认上来说,我们希望继承厂商提供的接口时应采用私有继承,从而让它不被孙子类使用。

    相关链接:

  • Bridge:将接口和实现分离。比如设计 GUI 程序时,我定义了一个 Window 抽象类,其中完成窗口的基本操作,如绘制、移动、最大化、最小化等,该抽象类有多个子类,比如对话框(DialogWindow)、无边框窗口等。但不同的平台下实现窗口基本操作的方法是不一样的,比如 x11 有某个实现,windows 有另一个实现等。这时我就可以在 Window 抽象类中包含一个 WindowImpl 类,而 WindowImpl 类是个抽象类,有多个子类,比如 XWindowImpl 等。
  • Composite:实现递归组合。当一个对象可能包含自身时即可使用此设计模式。例如对于一个编辑器,其内容可以是字符、图片、形状,然后又有行和列的概念,且一行包括多个字符、图片、形状,一列又包括多行,一列即可表达一页或半页文档(如果文档有两列),这时可以定义一个抽象类“图元”,上述所有类均继承自该抽象类,该抽象类有“父亲”和“孩子”,父亲只有一个,孩子有多个,均可定义为图元指针,以表达前述的包含(组合)关系。
  • Decorator:可以将被装饰者作为参数传入。
  • Facade:封装子系统常用的对外接口到一个类中。
  • Flyweight:降低内存消耗,采用类似于内存池的方式。
  • Proxy:类似网络代理。

行为型设计模式

  • Interpreter:解释器,用于语法解释,如正则表达式等。
  • Template Method:抽象类中的某个方法为实函数,其中调用了其他未实现的虚函数,这些虚函数交给子类实现,不同的子类可以有不同类的实现,但总体流程(位于抽象类的实函数中)是相同的
  • Chain of Responsibility:链式职责。将某个请求从某个对象链式传递到其他对象,中间只要有对象能处理,则中断传递。这可以让多个对象都有机会处理该请求。比如在 qt 中,对于鼠标事件,可以从当前对象链式传递到其 parent,其 parent 又可传递到它的 parent,直到“祖宗”(注意这里的 parent 不是父类,而是父对象,通常表达包含关系,例如一个窗口包含一个按钮,则该按钮的 parent 就是窗口)
  • Command:将命令(请求)封装为对象而非方法(函数)。
  • Iterator:将多样的遍历方法封装为统一的遍历方法。例如对于链表、数组的遍历方式通常有所不同,但只要使用 Iterator,则可以使用相同代码进行遍历:定义一个抽象类 Iterator,其中定义遍历接口,如 first(), next(), isLast() 等,然后实现子类 LinkedIterator 和子类 ArrayIterator,实现以上接口,使用时直接用抽象类调用接口实现遍历,例如:

    1
    2
    3
    4
    5
    6
    
    Iterator<int>* it = new LinkedIterator<int>();
    while(!it.isLast()) {
      int item = it.next();
      // do something with item...
    
    }
    
  • Mediator:实现集中式通信,避免多个对象间混乱的分布式通信。比如我有多个对象,它们互相依赖别人的信息,这时我可以创建一个 Mediator,所有对象都和它通信,它负责中转(有点像交换机?),这样一来,所有对象间的混乱依赖就不存在了,所有对象都会依赖且仅依赖 Mediator。
  • Memento:提取出内部状态或者从提取出的状态恢复。
  • Observer:MVC 模型中就有此模式,Model 表达一份数据,View 展示多个视图(界面),使用的是同一份数据,数据一旦发生修改,所有视图同步修改。其中 Model 是被观察者,View 是观察者,观察者发现被观察者发生变化就同步更新。
  • State:将状态和行为关联起来(有点像状态机?)
  • Strategy:将算法封装为类而非方法。因为面向对象思想中,类是最小单位,方法是类中的一部分,所以要想复用,就要创建相应的类。
  • Visitor:对遍历中取得的成员做同一操作,比如打印每个成员,或者将某个集合映射到另一集合,通过使用某个算法或者函数。把某个集合中的成员作为输入,进行一定的处理。其中“成员”可能有多种,但继承自同一抽象类(如 Node),“一定的处理”同样可能有多种,但同样继承自同一抽象类(如 NodeVisitor)。

心得

设计模式非常有意思,它不是那种你学了就能马上理解并用上的知识,而是需要你不断体会、不断尝试才能加深理解、灵活应用的知识。所以一开始不太明白没关系,后续在大量阅读开源代码及自身实践后总会逐渐领悟其奥妙。

本文由作者按照 CC BY 4.0 进行授权