简介

设计模式是什么?

  • 设计模式,是软件设计中常见问题的典型解决方案。它们就像能根据需求进行调整的预制蓝图,可用于解决代码中反复出现的设计问题。

  • 设计模式与方法或库的使用方式不同,你很难直接在自己的程序中套用某个设计模式。
  • 模式并不是一段特定的代码,而是解决特定问题的一般性概念。你可以根据模式来实现符合自己程序实际所需的解决方案
    • 类库,是由程序组合而成的组件,
    • 而设计模式则用来表现内部组件是符合被组装的,以及每一个组件是如何通过相互关联来构成一个庞大系统的
  • 人们常常会混淆模式和算法,因为两者在概念上都是已知特定问题的典型解决方案。
  • 但算法总是明确定义达成特定目标所需的一系列步骤,而模式则是对解决方案的更高层次描述。同一模式在两个不同程序中的实现代码可能会不一样

  • 设计模式的目标之一,就是提高程序的可复用性

  • 算法更像是菜谱:提供达成目标的明确步骤。
  • 而模式更像是蓝图:你可以看到最终的结果和模式的功能,但需要自己确定实现步骤

  • 模式包含哪些内容?
    • 意图部分,简单描述问题和解决方案
    • 动机部分,将进一步解释问题并说明模式会如何提供解决方案
    • 结构部分,展示模式的每个部分和它们之间的关系
    • 在不同语言中的实现提供流行编程语言的代码, 让读者更好地理解模式背后的思想

模式的历史

  • 谁发明了设计模式? 这是一个很好的问题, 但也有点不太准确。
  • 设计模式并不是晦涩的、复杂的概念——事实恰恰相反。模式是面向对象设计中常见问题的典型解决方案。同样的解决方案在各种项目中得到了反复使用,所以最终有人给它们起了名字,并对其进行了详细描述。这基本上就是模式被发现的历程了

  • 模式的概念是由克里斯托佛·亚历山大在其著作《建筑模式语言》中首次提出的。本书介绍了城市设计的 “语言”, 而此类 “语言” 的基本单元就是模式。模式中可能会包含对窗户应该在多高、一座建筑应该有多少层以及一片街区应该有多大面积的植被等信息的描述

  • 埃里希·伽玛、约翰·弗利赛德斯、拉尔夫·约翰逊和理查德·赫尔姆这四位作者接受了模式的概念。 1994 年,他们出版了《设计模式: 可复用面向对象软件的基础》一书,将设计模式的概念应用到程序开发领域中。
  • 该书提供了 23 个模式来解决面向对象程序设计中的各种问题,很快便成为了畅销书。由于书名太长,人们将其简称为 “四人组 (Gang of Four, GoF) 的书”, 并且很快进一步简化为 “GoF 的书

  • 此后,人们又发现了几十种面向对象的模式。“模式方法” 开始在其他程序开发领域中流行起来。如今,在面向对象设计领域之外,人们也提出了许多其他的模式

为什么以及如何学习设计模式?

  • 设计模式是针对软件设计中常见问题的工具箱,其中的工具就是各种经过实践验证的解决方案。 即使你从未遇到过这些问题,了解模式仍然非常有用,因为它能指导你如何使用面向对象的设计原则来解决各种问题

  • 设计模式定义了一种让你和团队成员能够更高效沟通的通用语言。 你只需说 “哦, 这里用单例就可以了”, 所有人都会理解这条建议背后的想法。 只要知晓模式及其名称, 你就无需解释什么是单例

关于模式的争议

  • 一种针对不完善编程语言的蹩脚解决方案
    • 通常当所选编程语言或技术缺少必要的抽象功能时, 人们才需要设计模式。 在这种情况下, 模式是一种可为语言提供更优功能的蹩脚解决方案
    • 例如, 策略模式在绝大部分现代编程语言中可以简单地使用匿名 (lamb­da) 函数来实现。
  • 低效的解决方案
    • 模式试图将已经广泛使用的方式系统化。 许多人会将这样的统一化认为是某种教条, 他们会 “全心全意” 地实施这样的模式, 而不会根据项目的实际情况对其进行调整
  • 不当使用
    • 如果你只有一把铁锤, 那么任何东西看上去都像是钉子。
    • 这个问题常常会给初学模式的人们带来困扰: 在学习了某个模式后, 他们会在所有地方使用该模式, 即便是在较为简单的代码也能胜任的地方也是如此

设计模式分类

  • 不同设计模式的复杂程度、 细节层次以及在整个系统中的应用范围等方面各不相同

  • 最基础的、 底层的模式通常被称为惯用技巧。 这类模式一般只能在一种编程语言中使用

  • 最通用的、 高层的模式是构架模式。 开发者可以在任何编程语言中使用这类模式。 与其他模式不同, 它们可用于整个应用程序的架构设计。

  • 此外, 所有模式可以根据其意图或目的来分类。 本书覆盖了三种主要的模式类别:

    • 创建型模式提供创建对象的机制, 增加已有代码的灵活性和可复用性。
    • 结构型模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。
    • 行为模式负责对象间的高效沟通和职责委派。

Iterator模式

  • for循环语句遍历数组,for语句中的i++的作用是让i的值在每次循环后自增1,这样就可以访问数组的下一个元素,下下一个元素,再下下一个元素,也就实现了从头至尾逐一遍历的功能

  • 将这里的循环变量i的作用抽象化,通用化后形成的模式,在设计模式中成为Iterator模式

  • Iterator模式,用于在数据集合中按照循序遍历集合。英文单词Iterate有反复做某件事情的意思,汉语称为“迭代器”

  • 为什么一定要考虑引入Iterator这种复杂的设计模式?如果是数组,直接使用for循环语句进行遍历处理不就可以了?
    • 一个重要的理由是,引入Iterator后可以将遍历与实现分离开来
  • 设计模式的作用就是帮助编写可复用的类,
  • 所谓“可复用”,就是指将类实现为“组件”,当一个组件发生改变时,不需要对其他的组件进行修改或者只是需要很小的修改即可应对。

  • 抽象类和接口
    • 人们总想用具体的类来解决所有的问题
    • 但是如果只使用具体的类来解决问题,很容易导致类之间的强耦合,这些类也难以作为组件被再次利用。
    • 为了弱化类之间的耦合,进而使得类更加容易作为组件被再次利用,需要引入抽象类接口
  • 不要只使用具体类来变成,要优先使用抽象类和接口来编程

Adapter模式

  • 在程序世界中,经常会存在现有的程序无法直接使用,需要做适当的变换之后才能使用的情况。
  • 这种用于填补“现有的程序”和“所需的程序”之间差异的设计模式就是Adapter模式

  • Adapter模式,也被称为Wrapper模式。Wrapper有“包装器”的意思,就像精美的包装纸将普通商品包装成礼物那样,替我们把某样东西包起来,使其能够用于其他用途的东西就被称为“包装器”或者是“适配器”

  • Adapter模式有两种
    • 类适配器模式(使用继承的适配器)
    • 对象适配器模式(使用委托的适配器)
      • 委托,通俗来讲,就是交给其他人,在Java语言中,委托就是将某个方法中的实际处理交给其他实例的方法
  • 什么时候使用Adapter模式?
    • 有人认为:如果某个方法就是我们所需要的方法,那么直接在程序中使用不就可以了?为什么还要考虑使用Adapter模式呢?
    • 很多时候,我们并非从零开始编程,经常会用到现有的类。
    • 特别是当现有的类已经被充分测试过了,Bug很少,而且已经被用于其他软件时,我们更愿意将这些类作为组件重复利用
    • Adapter模式会对现有的类进行适配,生成新的类。通过该模式可以很方便地创建我们需要的方法群
  • 版本升级和兼容性
    • 版本的生命周期总是伴随版本的升级,而在版本升级的时候经常会出现“与旧版本的兼容性”的问题。
    • 如果能够完全抛弃旧版本,那么软件的维护工作将会轻松很多,但是现实中往往无法这样做。
    • 这时,可以使用Adapter模式使新旧版本兼容,帮助我们轻松的同时维护新版本和旧版本
  • 功能完全不同的类,Adapter模式是无法使用的

Template Method模式

  • 什么是模板?
    • 模板的愿意是指带有镂空文字的薄薄的塑料板
  • 什么是Template Method模式?
    • Template Method模式是带有模板功能的模式,组成模板的方法被定义在父类中。
    • 由于这些方法是抽象方法,所以只查看父类的代码是无法知道这些方法最终会进行何种具体处理的,唯一能够知道的就是父类是如何调用这些方法的。
  • 实现上述这些抽象方法的是子类。在子类中实现了抽象方法也就决定了具体的处理。也就是说,只要在不同的子类中实现不同的具体处理,当父类的模板方法被调用时程序行为也会不同。
  • 但是,不论子类中的具体实现如何,处理的流程都会按照父类中所定义的那样进行。
  • 像这样在父类中定义处理流程的框架,在子类中实现具体处理的模式就称为Template Method模式

延伸阅读:类的层次与抽象类

  • 我们在理解类的层次时,通常是站在子类的角度进行思考的。也就是说,很容易着眼于以下几点
    • 在子类中可以使用父类中定义的方法
    • 可以通过在子类中增加方法以实现新的功能
    • 在子类中重写父类的方法可以改变程序的行为
  • 改变一下立场,站在父类的角度进行思考。在父类中,我们声明了抽象方法,而将该方法的实现交给了子类。换言之,就程序而言,声明抽象方法是希望达到以下目的
    • 期待子类去实现抽象方法
    • 要求子类去实现抽象方法
  • 也就是说,子类具有实现父类中所声明的抽象方法的责任。因此,这种责任被称为“子类责任(subclass responsibility)”

  • 抽象类的意义
    • 对于抽象类,我们是无法生成其实例的。
    • 由于在抽象方法中并没有编写具体的实现,所以我们无法知道在抽象方法中到底进行了什么样的处理。
    • 但是我们可以决定抽象方法的名字,然后通过调用使用了抽象方法的模板方法去编写处理。虽然具体的处理内容是由子类决定的,不过在抽象类阶段确定处理的流程非常重要。

父类与子类之间的写作

  • 父类与子类的相互协作支撑起了整个程序。
  • 虽然将更多方法的实现放在父类中会让子类变得更轻松,但是同时也降低了子类的灵活性
  • 如果父类中实现的方法过少,子类就会变得臃肿不堪,而且还会导致各子类间的代码出现重复

Factory Method模式

  • Factory有“工厂”的意思。用Template Method模式来构建生成实例的工厂,这就是Factory Method模式

  • 在Factory Method模式中,父类决定实例的生成方式,但是并不决定所要生成的具体的类,具体的处理全部交给子类负责。这样就可以将生成实例的框架(framework)和实际负责生成实例的类解耦

使用模式与开发人员之间的沟通

  • 无论是Template Method模式还是Factory Method模式,在实际工作中使用时,都会让我们感到比较困难。
  • 这是因为,如果仅阅读一个类的代码,是很难理解这个类的行为的。必须要理解父类中所定义的处理的框架和它里面所使用的的抽象方法,然后阅读代码,了解这些抽象方法在子类中的实现才行。

Singleton模式

  • 想确保任何情况下都绝对只有1个实例,在程序上表现出“只存在一个实例”。想这样的确保只生成一个实例的模式被称为Singleton模式。
  • Singleton是指只包含有一个元素的集合。

  • 为什么必须设置限制?
    • 设置限制其实就是为程序增加一项前提条件
    • 当存在多个实例时,实例之间相互影响,可能会产生意想不到的Bug
    • 但是,如果我们可以确保只有一个实例,就可以在这个前提条件下放心地编程

Builder模式

  • Builder模式:用于组装具有复杂结构的实例

谁知道什么

  • 在面向对象编程中,“谁知道什么”是非常重要的。也就是说,我们需要在编程时注意哪个类可以使用哪个方法以及使用这个方法到底好不好

设计时能够决定的事情和不能决定的事情

  • 虽然类的设计者并不是神仙,无法准确地预测到将来可能发生的变化。但是,还是有必要让设计出的类能够尽可能灵活地应对近期可能发生的变化

代码的阅读方法和修改方法

  • 在编程时,虽然有时需要从零开始编写代码,但更多时候我们都是在现有代码的基础上进行增加和修改
  • 这时,我们需要先阅读现有代码。不过,只是阅读抽象类的代码是无法获取很多信息的(虽然可以从方法名中获得线索)

  • 如果没有理解各个类的角色就动手增加和修改代码,在判断到底应该修改哪个类时,就会很容易出错

Abstract Factory模式

  • Abstract的意思是“抽象的”,Factory的意思是“工厂”,将他们组合起来就可以知道Abstract Factory表示“抽象工厂”的意思。
  • 抽象工厂的工作是将“抽象零件”组装为“抽象产品”

  • 面向对象编程中的“抽象”
    • 它指的是“不考虑具体怎样实现,而是仅关注接口(API)”的状态
    • 例如,抽象方法(Abstract Method)并不定义方法的具体实现,而是仅仅只确定了方法的名字和签名(参数的类型和个数)

Bridge模式

  • Bridge的意思是“桥梁”。就像在现实世界中,桥梁的功能是将河流的两侧连接起来一样,Bridge模式的作用也是将两样东西连接起来,他们分别是类的功能层次结构和类的实现层次结构
  • Bridge模式的作用是在“类的功能层次结构”和“类的实现层次结构”之间搭建桥梁,换句话说,将类的功能层次结构与实现层次结构分离

  • 类的功能层次结构
    • 假设现在有一个类Something.当我们想在Something中增加新功能时(想增加一个具体方法时),会编写一个Something类的子类(派生类),即SomethingGood类。这样就构成了一个小小的类层次结构
    • 这就是为了增加新功能而产生的层次结构:父类具有基本功能,在子类中增加新的功能
    • 以上这种层次结构被称为类的功能层次结构
    • 当要增加新的功能时,可以从各个层次的类中找出最符合自己需求的类,然后以它为父类编写子类,并在子类中增加新的功能。这就是类的功能层次结构
  • 类的实现层次结构
    • 抽象类声明了一些抽象方法,定义了接口(API),然后子类负责去实现这些抽象方法。父类的任务是通过声明抽象方法的方式定义接口(API),而子类的任务是实现抽象方法。正式由于父类和子类的这种任务分担,我们才可以编写出具有高可替换性的类
    • 这里其实也存在层次结构。例如,当子类ConcreteClass实现了父类AbstractClass类的抽象方法时,它们之间就构成了一个小小的层次结构。但是,这里的类的层次结构并非用于增加功能,也就是说,这种层次结构并非用于方便我们增加新的方法。它的真正作用是帮助我们实现两个任务分担
      • 父类通过声明抽象方法来定义接口(API)
      • 子类通过实现具体方法来实现接口(API)
    • 这种层次结构被称为类的实现层次结构
  • 类的层次结构的混杂与分离
    • 当我们想要编写子类时,就需要先确认自己的意图:是要增加功能呢?还是要增加实现呢?
    • 当类的层次结构只有一层时,功能层次结构与实现层次结构是混杂在一个层次结构中的。这样很容易使类的层次结构变得复杂,也难以透彻地理解类的层次结构。因为自己难以确认究竟应该在类的哪一个层次结构中去增加子类。
    • 因此,我们需要将“类的功能层次结构”与“类的实现层次结构”分离为两个独立的类层次结构。当然,如果只是简单地将他们分开,两者之间必然会缺少联系,所以我们还需要在他们之间搭建一座桥梁,而Bridge模式的作用就是搭建这座桥梁
  • Bridge模式的特征是将“类的功能层次结构”与“类的实现层次结构”分离开了。将类的这两个层次结构分离开有利于独立地对他们进行扩展
  • 当想要增加功能时,只需要在“类的功能层次结构”一侧增加类即可,不必对“类的实现层次结构”做任何修改。而且,增加后的功能可以被“所有的实现”使用

  • 继承是强关联关系,委托是弱关联关系。在设计类的时候,我们必须充分理解这一点

Strategy模式

  • Strategy的意思是“策略”,指的是与敌军对垒时行军作战的方法。**在编程中,我们可以将它理解为“算法”

  • 无论什么程序,其目的都是解决问题。而为了解决问题,我们又需要编写特定的算法。
  • 使用Strategy模式,可以整体地替换算法的实现部分。能够整体地替换换发,能让我们轻松地以不同的算法去解决同一个问题,这种模式就是Strategy模式

Composite模式

  • 在计算机的文件系统中,有文件夹的概念(在有些操作系统中,也称为目录)。文件夹里面既可以放入文件,也可以放入其他文件夹(子文件夹)。
  • 在子文件夹中,一样地既可以放入文件,也可以放入子文件夹。可以说,文件夹是形成了一种容器结构,递归结构
  • 文件夹和文件有时也被统称为“目录条目”(directory entry)。在目录条目中,文件夹和文件被当作是同一种对象看待(即一致性)

  • 有时,与将文件夹和文件都作为目录条目看待一样,将容器和内容作为同一种东西看待,可以帮助我们方便地处理问题。

  • 能够使容器与内容具有一致性,创造出递归结构的模式就是Composite模式。Composite在英文中是混合物,复合物的意思

Decorator模式

  • 不断地为对象添加装饰的设计模式,被称为Decorator模式。Decorator指的是“装饰物”

Visitor模式

  • 在Visitor模式中,数据结构与处理被分离开来。
  • 编写一个表示“访问者”的类来访问数据结构中的元素,并把对各元素的处理交给访问者类。这样,当需要增加新的处理时,我们只需要编写新的访问者,然后让数据结构可以接受访问者的访问即可

  • 双重分发
    • accept(接受)方法的调用方式:element.accept(visitor);
    • 而visit(访问)方法的调用方式:visitor.visit(element);
    • 对比两个方法会发现,他们是相反的关系。element接受visitor,而visitor又访问element
    • 在Visitor模式中,ConcreteElement和ConcreteVisitor这两个角色共同决定了实际进行的处理,这种消息分发的方式一般被称为双重分发(double dispatch)
  • Visitor模式的目的是将处理从数据结构中分离出来。数据结构很重要,它能将元素集合和管理在一起。
  • 但是,需要注意的是,保存数据结构与以数据结构为基础进行处理是两种不同的东西

Observer模式

  • Observer的意思就是“进行观察的人”,也就是“观察者”的意思
  • 在Observer模式中,当观察对象的状态发生变化时,会通知给观察者。Observer模式适用于根据对象状态进行相应处理的场景。

  • 利用抽象类和接口从具体类中抽出抽象方法
  • 在将实例作为参数传递至类中,或者在类的字段中保存实例时,不使用具体类型,而是使用抽象类型的接口
  • 这样的实现方式可以帮助我们轻松替换具体类

State模式

  • 在面向对象编程中,是用类表示对象的。
  • 也就是说,程序的设计者需要考虑用类来表示什么东西。类对应的东西可能存在于现实世界中,也可能不存在于现实世界中。对于后者,可能有人看到代码后会感到吃惊:这些东西居然也可以是类啊

  • 分而治之
    • 在编程时,会经常使用分而治之的方针。它非常适用于大规模的复杂处理。当遇到庞大且复杂的问题,不能用一般的方法解决时,会先将问题分解为多个小问题。如果还是不能解决这些小问题,会将它们继续划分为更小的问题,直至可以解决它们为止。
    • 分而治之,简单而言,就是将一个复杂的大问题分解为多个小问题然后逐个解决。
  • 在State模式中,用类来表示状态,并为每一种具体的状态都定义一个相应的类。这样问题就被分解了
  • 换言之,State模式用类表示系统的“状态”,并以此将复杂的程序分解开来

  • 在State模式中,我们应该如何编程,以实现“依赖于状态的处理”呢?总结起来有如下两点
    • 定义接口,声明抽象方法
    • 定义多个类,实现具体方法

Flyweight模式

  • Flyweight是“轻量级”的意思,指的是拳击比赛中选手体重最轻的登记。顾名思义,该设计模式的作用是为了让对象变“轻”
  • 对象在计算机中是虚拟存在的东西,它的“重”和“轻”并非指实际重量,而是它们“所使用的的内存大小”。使用内存多的对象就是“重”对象,使用内存小的对象就是“轻”对象

  • 为了能够在计算机中保存该对象,需要分配给其足够的内存空间。当程序中需要大量对象时,如果都是用new关键字来分配内存,将会消耗大量内存空间
  • 关于Flyweight模式,一言以蔽之就是 : 通过尽量共享示例来避免new出实例

  • Intrinsic与Extrinsic
    • 应当共享的信息被称作Intrinsic信息。Intrinsic的意思是“本质的,固有的”。
      • 换言之,它指的是不论实例在哪里,不论在什么情况下都不会改变的信息,或者是不依赖于实例的信息
    • 不应当共享的信息被称作Extrinsic信息。Extrinsic的意思是“外在的,非本质的”。
      • 也就是说,它是当实例的位置,状况发生改变时会变化的信息,或是依赖于实例状态的信息

Proxy模式

  • Proxy是“代理人”的意思,它指的是代替别人进行工作的人。
  • 在面向对象编程中,“本人”和“代理人”都是对象。如果“本人”对象太忙了,有些工作无法自己亲自完成,就将其交给“代理人”对象负责

Command模式

  • 一个类在进行工作时会调用自己或是其他类的方法,虽然调用结果会反应在对象的状态中,但并不会留下工作的历史记录。
  • 这时,如果我们有一个类,用来表示“请进行这项工作”的“命令”就会方便很多。每一项想做的工作就不再是“方法的调用”这种动态处理了,而是一个表示命令的类的实例,即–可以用“物”来标识。要想管理工作的历史记录,只需管理这些实例的集合即可,而且还可以随时再次执行过去的命令,或是将多个过去的命令整合为一个新命令并执行。
  • 在设计模式中,称这样的“命令”为Command模式

Q&A

  • 设计模式能够解决软件开发中的所有问题吗?
    • 不能,每个设计模式都是用于解决软件开发过程中遇到的问题,但是无论使用什么解决方法,都需要从整体权衡。设计模式并不能解决所有问题
  • 怎样才能选择出合适的设计模式呢?
    • 首先必须要明确知道自己的软件中存在什么样的问题。如果问题不够明确,是无法选择出合适的设计模式的。
    • 在学习设计模式时,我们要注意该模式“可以解决什么问题”
  • 所谓设计模式,其解决方法都是理所当然的,并不认为有值得关注和重新学习的价值。为什么设计模式很重要呢?
    • 在向经验丰富的开发人员介绍设计模式时,他们会认为这是“理所当然”的。当然是这样的,因为本来设计模式就是开发人员对反复遇到的问题总结出来的解决办法
    • 设计模式的重要性在于,可以帮助大家很快地掌握那些经验丰富的开发人员才具有的知识和经验
  • 设计模式很难背下来
    • 机械的背下来这些设计模式是没有意义的。重要的是在自己脑海中理解设计模式是怎样解决问题的
  • 设计模式对初级开发人员也有帮助吗?
    • 对于刚刚掌握了编程语言,并逐渐开始慢慢编写一些程序的初级开发人员来说,通过设计模式可以学习到“在进行面向对象编程时,应该注意什么
    • 例如,通过设计模式,我们可以学到本书中讲解过得可复用性,可替换性,接口(API),继承和委托,抽象化等
    • 此外,设计模式的知识也会对我们自己使用类库有所帮助。这是因为类库中的许多部分都与设计模式有关
  • 除了“设计模式”外,还常听到“模式”这个词,两者的意思是相同的吗?
    • 严格来讲,两者的意思是有区别的
    • 不论是在什么领域,给“在某种场景下重复发生的问题的解决办法”赋予名字,并整理而成的东西一般都称为“模式”
    • 设计模式,是适用于软件设计和开发领域的模式,它是模式中的一种
    • 不过,有时候在软件领域也会将“设计模式”简称为模式

参考书籍

  • 《深入设计模式》

  • https://refactoringguru.cn/design-patterns