$cover

设计模式学习笔记

前言

之前去面试迅雷实习生,被问到设计模式相关的问题,竟然一时语塞,脑海里只有「工厂模式」、「简单工厂模式」之类的概念,却不能用清晰的语言表述出来。在面试的间隙看了一眼相关的资料,发现很多设计模式在日常的学习中已经用到过,但并没有系统的整理学习过————于是现在有了这篇博客。

参考资料:

  1. 《松本行弘的程序世界》第四章 设计模式
  2. AlloyTeam的博客
  3. Design Pattern - Read the Docs

因为意识不到应该使用的模式,就会错过最合适的设计。——《松本行弘的程序世界》

什么是模式?

通常,建筑物的设计各不相同,另外加上用途、建筑条件等各种制约因素,一个设计是不能一成不变地套用到别的建筑物上的。建筑设计师只是通过重用积累的设计模式,试图缩短设计所花费的时间。

在软件设计中,最常见的重用方式是使用库。各种软件通过库共享处理过程、数据结构以及类等。

但是,只有库还不能达到充分的共享。软件设计中有些东西虽然不能以库的形式来把它独立出来,但是多个软件共通利用的东西是确实存在的。这样的东西只能称之为固定形式——模式

一种最简单的模式:

for (let i = 0; i < len; i++) {
    ...
}

在软件设计中像这样的模式数不胜数。不过,设计人员自己很少能够意识到模式的存在,一般是在积累了很多经验之后,才几乎在无意识之中利用模式提高了软件开发的效率。

Erich Gamma 与几位合作者一起,精选了软件设计中,特别是面向对象软件设计中反复出现的各种模式,比照着 Alexander 提倡的建筑上的概念,称之为「设计模式」。他们从自己及周围的开发经验中把模式总结出来并进行分类,出版了《设计模式》一书。

Erich Gamma 总结的 23 种设计模式

模式名内容
Abstract Factory(抽象工厂)用可配置的方法生成有关的对象群
Adapter(适配器)变换对象的接口
Bridge(桥接)分离类之间的实现
Builder(生成器)分离复杂对象的生成过程
Chain of Responsibility(职责链)用多个对象来处理请求
Command(命令)把请求封装成对象
Composite(组合)用树结构来构成对象
Decorator(装饰)给对象动态增加新的功能
Facade(外观)隐藏子系统的详细内容,提供统一的接口
Factory Method(工厂方法)在父类只定义生成对象的接口,具体的生成过程由派生类来实现
Flyweight(享元)以共享的方式提高大量小对象的实现效率
Interpreter(解释器)语言解释器
Iterator(迭代器)提供按顺序访问一组对象的方法
Mediator(中介者)封装对象之间的相互作用
Memento(备忘录)记录对象的内部状态
Observer(观察者)把对象的状态变更通知给其他对象
Prototype(原型)提供生成对象的原型
Proxy(代理)提供控制对象访问的代理(容器)
Singleton(单件)用来保证某个类的实例只有一个
State(状态)把对象的内部状态独立出来,封装状态变化
Strategy(策略)封装算法,使之具有可变换性
Template Method(模板方法)父类定义框架,派生类具体实现其中一部分
Visitor(访问者)对集合的元素进行操作

设计模式的意义和价值

引用自《松本行弘的程序世界》:

首先,它明确了各种模式的效果和适用状况,给众多模式命名并进行分类,这本身就是非常有意义的。如果没有名字的话,即使使用了模式,程序员也大都没有明确意识到使用了模式。因为意识不到应该使用的模式,就会错过最合适的设计。

但是,他们最大的功绩并不在于把设计模式进行分类,而是明确了「软件中可以分类的设计模式」的存在。有了这样的认识之后,设计模式就不仅限于书中介绍的23个,人们开始发现和定义更多设计模式。

设计模式有了名字,人们就可以认识到它的存在。对于没有名字的东西,人们几乎不可能认识到它的存在,并对之进行讨论。这种不能用语言表达的知识我们称之为内隐知识。给这些在软件中经常反复出现的模式命名,使得这些本来只有经验丰富的程序员才能认识到的软件设计模式能被广泛认识和讨论。这样的知识称为形式知识。把迄今为止普通人难于学习和吸收的内隐知识变成形式知识的功绩是无与伦比的。

简单工厂模式 & 工厂方法模式 & 抽象工厂模式

  • 简单工厂模式:
    • 通过传递参数,判断参数的值来返回不同类的实例。
    • 优点:使用方便,只需传入参数即可。
    • 缺点:逻辑集中,出现问题整个系统受影响;难以扩展,扩展需要修改判断的逻辑,不符合开闭原则。
    • 适用于工厂类负责创建的对象较少的情况。
      function buttonFactory (String type) {
      if (type == "RedButton") {
       ...
       return new RedButton();
      } else if (type == "BlueButton") {
       ...
       return new BlueButton();
      }
      }
      
  • 工厂方法模式:
    • 父类提供公共接口,子类负责生产具体对象。
    • 优点:无需关心产品,只需关心对应的工厂;可扩展性好,加入新产品只需添加具体工厂和具体产品,无需修改其他,符合开闭原则。
    • 缺点:类的数量增加(每个产品都要提供对应的工厂类),增加了抽象性和复杂度。
      class ButtonFactory {
      onClick() {
       ...
      }
      }
      class RedButtonFactory extends ButtonFactory {
      constructor: () => { 
       ... 
       retrun RedButton;
      }
      }
      class BlueButtonFactory extends ButtonFactory {
      ...
      constructor: () => {
       ...
       return BlueButton;
      }
      }
      
  • 抽象工厂模式:
    • 在工厂方法模式的基础上,一个工厂可以提供多个产品对象。
    • 由此衍生出了产品等级结构(继承结构)产品族(不同等级结构中的一组产品)
    • 优点:扩展方便,无须修改已有系统,符合开闭原则;高内聚低耦合;当一个产品族中的多个对象被设计成一起工作时,能够保证客户端始终只使用同一个产品族中的对象。
    • 缺点:加新的工厂和产品族容易,增加新的产品等级结构麻烦。
      class Theme {
      CreateButton();
      CreateBackground();
      CreateNav();
      }
      class RedTheme {
      CreateButton() {
       return RedButton;
      }
      CreateBackground() {
       return RedBackground;
      }
      CreateNav() {
       return RedNav;
      }
      }
      class BlueTheme {
      CreateButton() {
       return BlueButton;
      }
      CreateBackground() {
       return BlueBackground;
      }
      CreateNav() {
       return BlueNav;
      }
      }
      

工厂模式的退化

  • 当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;
  • 当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。

持续更新中…

单例模式

  • 单例模式的要点有三个:
    • 一是某个类只能有一个实例;
    • 二是它必须自行创建这个实例;
    • 三是它必须自行向整个系统提供这个实例。
  • 单例类一般是全局的。
  • 优点:提供了对唯一实例的受控访问;对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
  • 缺点:单例类的职责过重,在一定程度上违背了「单一职责原则」。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起,这也导致它难以扩展;
  • 滥用单例模式的问题:实例化对象长时间不使用,可能导致运行环境自动回收垃圾,销毁对象。下次再调用时又重新创建,这导致了状态丢失。

建造者模式