设计模式


摘要

GoF对于设计模式提供了如下定义:

软件设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被人理解并且保证代码的可靠性。

在GoF提出的23中设计模式可以大致分为三种(GoF23+简单工厂=24):

  • 创建型模式:如何创建对象
    • 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点
    • 简单工厂模式:通过专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类
    • 工厂方法模式 :定一个创建产品对象的工厂接口,将实际创建工作推迟到子类中
    • 抽象工厂模式:提供一个创建一些系列相关或者相互依赖的接口,而无需指定它们具体的类
    • 原型模式:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象
    • 建造者模式:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表达
  • 行为型模式:类或者对象如何交互以及如何分配职责
    • 适配器模式:将一个类接口转换成客户希望的另一个接口。使得原本由于接口不兼容而不能一起工作的类可以一起工作
    • 桥接模式:将抽象部分与实际部分分离,使它们都可以独立的变化
    • 组合模式:将对象组合成树形结构以表示“部分–整体”的层次结构。使得用户对单个对象和组合对象的使用具有一致性
    • 装饰模式:动态的给一个对象添加一些额外的职责。就增加功能来说,此模式比生成子类更为灵活
    • 外观模式:为子系统中的一组接口提供一个一致的页面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
    • 享元模式:以共享的方式高效的支持大量细粒度的对象
    • 代理模式:为其他对象提供一种代理以控制对这个对象的访问
  • 结构性模式:如何实现类和对象的组合
    • 责任链模式:在该模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任
    • 命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户端进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作
    • 解释器模式:如何为简单的语言定义一个语法,如何在该语言中表示一个句子,以及如何解释这些句子
    • 迭代器模式:提供了一种方法顺序来访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示
    • 中介者模式:定义一个中介对象来封装系列对象之间的交互。中介者使各个对象不需要显示的相互调用 ,从而使其耦合性松散,而且可以独立的改变他们之间的交互
    • 备忘录模式:是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态
    • 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
    • 状态模式:对象的行为,依赖于它所处的状态
    • 策略模式:准备一组算法,并将每一个算法封装起来,使得它们可以互换
    • 模板方法模式:使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
    • 访问者模式:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作

前置知识点

  • Java接口不具有实现代码,继承接口无法达到代码复用。这意味着,无论何时你需要修改某个行为,你必须得往下追踪并在每一个定义此行为的类中修改它,一不小心,可能造成新的错误。

  • 良好的OO设计必须具有可复用、可扩充、可维护三个特性。

  • 模式被认为是历经验证的OO设计经验。

  • 代码应该免于改变但是能够拓展。

OOP原则

目标是:高内聚、低耦合

  • 单一职责原则:类的职责单一,对外只提供一种功能,而引起类变化的原因应该都只有一个
  • 开闭原则:类的改动是通过增加代码进行的,而不是修改源代码
  • 里式代替原则:任何抽象类(interface接口)出现的地方都可以用他的实现类进行替换,实际就是虚拟机制,语言级别实现面向对象功能
  • 依赖倒转原则:依赖于对象(接口),不要依赖具体的实现(类),针对接口编程
  • 接口隔离原则:不应该强迫用户依赖他们不需要的接口方法,一个接口应该只提供一种对外功能,不应该把所有操作都封装到一个接口中去
  • 合成复用原则:如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。对于继承和组合,优先使用组合
  • 迪米特原则:一个对象应该对其他类尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。例如在一个程序中,各个模块之间相互调用时,通常会提供一个统一的接口来实现。这样替他模块不需要了解另外一个模块的内部实现细节,这样当一个模块内部的实现出现改变时,不会影响其他模块的使用(黑盒原理)

创建型模式

创建型模式提供了创建对象的机制, 能够提升已有代码的灵活性和可复用性。

工厂模式

  • 静态工厂:采用静态方法定义一个简单工厂。这样不需要使用创建对象的方法来实例化对象。但是不能通过继承来改变创建方法的行为。
  • 设计模式中,所谓的“实现一个接口”并“不一定”是“写一个类,并利用implemennt关键词来实现某个java接口”。泛指“实现某个超类型(可以是类或接口)的某种方法”。

简单工厂

设计模式_6.png

  • 简单工厂如上图所示,并不是“工厂模式”。只是一种编程习惯。本质就是将针对实现的代码提取出来,进行封装。这里封装成了工厂而已。

    优点
    1. 实现了对象创建和使用的分离
    2. 不需要记住具体类名,记住参数即可,减少使用者记忆量
    缺点
    1. 对工厂类职责过重,一旦不能工作,系统收到影响
    2. 增加系统中类的个数,复杂度和理解度增加
    3. 违反开闭原则,增加新产品需要修改工厂逻辑,工厂越来越复杂
    适用场景
    1. 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂
    2. 客户端只知道传入工厂类的参数,对于如何创建对象并不关心

真正的工厂模式

  • 所有工厂模式都用来封装对象的创建。
  • 工厂模式方法通过让子类决定创建的对象是什么,来达到将对象创建的过程封装的目的。

设计模式_7.png

设计模式_8.png

定义

工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。这里的决定并不是指模式允许子类本身在运行时做决定,而是指在编写创建者类时,不需要知道实际创建的产品是哪一个。选择了使用哪个子类,自然就决定了实际创建的产品是什么。

设计模式_9.png

  • 即使在只有一个ConcreteCreator的时候,工厂方法模式依然有用。可以有效的帮助将产品的“实现”从“使用”中解耦。如果增加产品或者改变产品的实现,Creator不会受影响。
  • 工厂方法模式中的ConcreteCreator和Creator的实现类似见到简单工厂但是这里的ConcreteCreator扩展自Creator。每个ConcreteCreator自行负责具体实现方法。简单工厂中,Creator只是ConcreteCreator使用的对象。
  • 工厂方法和创建者不一定总是抽象的。可以定义一个默认的工厂方法来产生某些具体的产品。这样,即使创建者没有任何子类,依然可以创建产品。
  • 设计原则六:(依赖倒置原则)要依赖抽象,不要依赖具体类。
  • 设计原则六说明了不能让高层组件依赖低层组件,且都应该依赖抽象。所谓高层组件指的是低层组件定义其行为的类。

指导方针

以下方法可以避免在OO设计中违反依赖倒置原则:

  • 变量不可以持有具体类的引用。如果使用new就会持有具体类的引用,使用工厂来避免。
  • 不要让类派生自具体类。如果派生自具体类,就会依赖具体类。请派生一个接口或抽象类。
  • 不要覆盖基类中已实现的方法。如果覆盖基类中已实现的方法,那么你的基类就不是一个真正适合被继承的抽象。基类中已实现的方法,应该由所有的子类共享。

抽象工厂模式

定义

抽象工厂模式:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。

设计模式_10.png

抽象工厂的方法经常以工厂方法的方式实现。抽象工厂的任务是定义一个负责创建一组产品的接口。在这个接口内的每个方法都负责创建一个具体产品,同时利用实现抽象工厂的子类来提供这些具体的做法。

抽象工厂创建相关的对象家族,而不需要依赖它们的具体类。

工厂方法模式与抽象工厂模式的区别:

  • 抽象工厂使用的是对象之间的组合,对象的创建被实现在工厂接口所暴露出来的方法中。而工厂方法使用的是继承,把对象的创建委托给子类,子类实现工厂方法来创建对象。
  • 利用工厂方法创建对象,需要扩展一个类,并覆盖它的工厂方法。整个工厂方法模式,只不过就是通过子类来创建对象。
  • 抽象工厂提供一个用来创建一个产品家族的抽象类型,这个类型的子类定义了产品被产生的方法。要想使用这个工厂,必须先实例化它,然后将它传入一些针对抽象类型所写的代码中。可以把一群相关的产品结合起来。

所有的工厂都是来封装对象的创建。

所有工厂模式都通过减少应用程序和具体类之间的依赖促进松耦合。

设计模式_11.png

设计模式_12.png

单件模型

行为模式

策略模式(Strategy Pattern)

  • 设计原则一:找出应用中可能需要变化之处,把它们独立出去,不要和那些不需要变化的代码混在一起。
  • 设计原则二:针对接口编程,而不是针对实现编程。
  • 需要变化的模块将被放在分开的类中,此类专门提供某行为接口的实现,则相应的类不再需要知道行为的实现细节。
  • 以前的做法是:行为来自超类的具体实现,或是继承某个接口并由子类自行实现而来。这两种做法都依赖于“实现”。被实现绑得死死的,没办法更改行为(除非写更多代码)。现行做法是特定的具体行为编写在相应的接口的具体实现类中。
  • 针对接口编程指的是针对超类型(supertype)编程。优势是利用多态进而执行时会根据实际状态执行到真正的行为。更明确的说就是“变量的声明类型应该是超类型,通常是一个抽象类或者是一个接口。如此。只要是具体实现此超类型的类所产生的对象,都可以指定给这个变量。这意味者,声明类时不会理会以后执行时的真正对象类型。”

设计模式_1.png

  • 设计原则三:多用组合,少用继承。
  • 使用组合建立系统具有很大的弹性,不仅可以将算法族封装成类,更可以在运行时动态的改变行为,只要组合的行为对象符合正确的接口标准。

定义

策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的用户。

具体实现方法

  1. 首先在超类中将会变化的方法变成接口类型的变量,而不是具体实现类类型。
  2. 然后在超类的相应行为的方法中调用接口类型的实现方法名。
  3. 子类的构造函数中给接口类型的变量new一个具体实现类的实体,并通过这个实体定义一个方法不实现,只做调用相应实现类的方法。
  4. 生成超类的对接口类型变量成员的setter。从而支持在运行时对变化方法的修改。
// 策略接口声明了某个算法各个不同版本间所共有的操作。上下文会使用该接口来
// 调用有具体策略定义的算法。
interface Strategy is
    method execute(a, b)

// 具体策略会在遵循策略基础接口的情况下实现算法。该接口实现了它们在上下文
// 中的互换性。
class ConcreteStrategyAdd implements Strategy is
    method execute(a, b) is
        return a + b

class ConcreteStrategySubtract implements Strategy is
    method execute(a, b) is
        return a - b

class ConcreteStrategyMultiply implements Strategy is
    method execute(a, b) is
        return a * b

// 上下文定义了客户端关注的接口。
class Context is
    // 上下文会维护指向某个策略对象的引用。上下文不知晓策略的具体类。上下
    // 文必须通过策略接口来与所有策略进行交互。
    private strategy: Strategy

    // 上下文通常会通过构造函数来接收策略对象,同时还提供设置器以便在运行
    // 时切换策略。
    method setStrategy(Strategy strategy) is
        this.strategy = strategy

    // 上下文会将一些工作委派给策略对象,而不是自行实现不同版本的算法。
    method executeStrategy(int a, int b) is
        return strategy.execute(a, b)


// 客户端代码会选择具体策略并将其传递给上下文。客户端必须知晓策略之间的差
// 异,才能做出正确的选择。
class ExampleApplication is
    method main() is

        创建上下文对象。

        读取第一个数。
        读取最后一个数。
        从用户输入中读取期望进行的行为。

        if (action == addition) then
            context.setStrategy(new ConcreteStrategyAdd())

        if (action == subtraction) then
            context.setStrategy(new ConcreteStrategySubtract())

        if (action == multiplication) then
            context.setStrategy(new ConcreteStrategyMultiply())

        result = context.executeStrategy(First number, Second number)

        打印结果。

优缺点

优点
  • 你可以在运行时切换对象内的算法
  • 你可以将算法的实现和使用算法的代码隔离开
  • 你可以使用组合来代替继承
  • 开闭原则。你无需对上下文进行修改就能够引入新的策略
缺点
  • 如果你的算法极少发生变化,那么没有任务理由引入新的类和接口。使用该模式只会让程序过于复杂
  • 客户端必须知晓策略间的不同——它需要选择合适的策略
  • 许多现代编程语言支持函数类型功能,允许你在一组匿名函数中实现不同版本的算法。这样,你使用这些函数的方式就和使用策略对象时完全相同,无需借助额外的类和接口来保持代码简洁

观察者模式

  • 针对具体实现编程导致以后对修改时需要大量改动。
  • 尽量将类似的接口进行封装统一。
  • 主题是真正拥有数据的人,观察者是主题的依赖者,在数据变化时更新,这样比起让许多对象控制同一份数据来,可以得到更干净的OO设计。
  • 松耦合:当两个对象之间松耦合,它们依然可以交互,但是不太清楚彼此的细节。
  • 观察者模式提供了一种对象设计,让主题和观察者之间松耦合。松耦合设计更有弹性,更能应对变化。
  • 设计原则四:为交互对象之间的松耦合设计而努力。
  • 有多个观察者时,不可以依赖特定的通知次序。

定义

定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

  • 当新类型的观察者出现时,主题的代码不需要修改。假如有新的具体类需要当观察者,我们不需要为了兼容新类型而修改主题的代码,所有要做的就是在新的类里实现此观察者接口,然后注册为新的观察者即可。
  • 改变主题或观察者其中一方,并不会影响另一方。因为松耦合,只要遵循它们之间的接口,我们就可以自由地改变他们。
  • 设计原则四:为了交互对象之间的松耦合设计而努力。
  • subject在观察者具体类中保留,进行注册和删除。
  • JAVA API内置了观察者模式。java.util包(package)内包含最基本的Observer接口与Observable类。

具体实现方法

  1. 创建一个subject接口,所有内容的提供者都要实现这个接口。该接口中需要定义三个函数:registerObserver(Object o)removeObserver(Object o)notifyObservser()
  2. 具体内容提供者实现subject接口,且设置一个私有ArrayList变量来存放所有的观察者。在构造函数中初始化观察者列表。在相应接口方法的实现中操作观察者列表。
  3. 创建观察者的接口类,声明函数update()。其中变量列表为subject中所有内容。
  4. 创建具体的观察者实现观察者接口。设置一个私有的主题接口类型变量,方便进行注册或退出。自定义相应的update函数。

使用API的具体实现方法

  • 把对象变成观察者:实现观察者接口(java.util.Observer),然后调用任何Observable对象的addObserver()方法。当不想再当观察者时,调用deleteObserver()方法即可。
  • 观察者送出通知
    1. 利用扩展java.util.Observable接口产生“可观察者类”。
    2. 调用setChanged()方法,标记状态已经改变的事实。
    3. 调用两种notifyObservers()方法中的一个:notifyObservers()或notifyObservers(Object arg)。
  • 观察者接收通知:观察者实现update(Observable o,Object arg)方法。主题本身作为第一个变量,好让观察者知道是哪个主题通知它。第二个参数是传入notifyObservers()的数据对象,没有说明则为空。

setChanged()方法用来标记状态已经改变的事实。如果调用notifyObservers()之前没有调用setChanged(),观察者就不会被通知。

hasChanged()方法获取changed标志的当前状态。

JAVA API实现的缺点

java自带的可观察类是一个类,而不是一个接口,且没有实现一个接口。

  • java不支持多继承,在继承Observable类的同时,无法继承另一个超类,限制了Observable的复用潜力。
  • 没有相应的接口,无法创建自己的实现。
  • Observable API中的setChanged()方法是protected类型。除非继承自Observable,否则无法创建Observable实例并组合到你自己的对象中去。违反了多用组合,少用继承。

命令模式

迭代器模型

定义

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

迭代器模式把游走的任务放在迭代器上,而不是聚合上。这样简化了聚合的接口和实现,也让责任各得其所。

适用场景

当集合背后为复杂的数据结构,且你希望对客户端隐藏其复杂性时(出于使用便利性或安全性的考虑),可以使用迭代器模式。

迭代器封装了与复杂数据结构进行交互的细节,为客户端提供了多个访问集合元素的简单方法。这种方式不仅对客户端来说方便,而且能避免客户端在直接与集合交互时执行错误或有害的操作,从而起到保护集合的作用。

使用迭代器模式可以减少程序中重复的遍历代码。

重要迭代算法的体积非常庞大。当这些代码被放置在程序业务逻辑中时,它就会让原始代码的逻辑不清楚,降低其维护性。

如果你希望代码能够遍历不同的甚至是无法预知的数据结构,可以使用迭代器模式。

该模式为集合和迭代器提供了一些通用接口。 如果你在代码中使用了这些接口, 那么将其他实现了这些接口的集合和迭代器传递给它时, 它仍将可以正常运行。

实现方式

  1. 声明迭代器接口。 该接口必须提供至少一个方法来获取集合中的下个元素。 但为了使用方便, 你还可以添加一些其他方法, 例如获取前一个元素、 记录当前位置和判断迭代是否已结束。
  2. 声明集合接口并描述一个获取迭代器的方法。 其返回值必须是迭代器接口。 如果你计划拥有多组不同的迭代器, 则可以声明多个类似的方法。
  3. 为希望使用迭代器进行遍历的集合实现具体迭代器类。 迭代器对象必须与单个集合实体链接。 链接关系通常通过迭代器的构造函数建立
  4. 在你的集合类中实现集合接口。 其主要思想是针对特定集合为客户端代码提供创建迭代器的快捷方式。 集合对象必须将自身传递给迭代器的构造函数来创建两者之间的链接
  5. 检查客户端代码, 使用迭代器替代所有集合遍历代码。 每当客户端需要遍历集合元素时都会获取一个新的迭代器

优点

  • 单一职责原则。通过将提及庞大的遍历算法代码抽取为独立的类,可对客户端代码和集合进行整理
  • 开闭原则。可实现新型的集合和迭代器并将其传递给现有代码,无需修改现有代码
  • 你可以并行遍历统一集合,每个迭代器对象都包含其自身的遍历状态
  • 相似的,你可以暂停遍历并在需要时继续

缺点

  • 如果你的程序只与简单的集合进行交互,应用该模式可能矫枉过正
  • 对于某些特殊集合,使用迭代器可能必直接遍历的效率低

结构型模式

装饰者模式

  • 设计原则五:类应该对扩展开放,对修改关闭。
  • 在选择需要被扩展的代码部分时要小心。每个地方都采用开放-关闭原则,是一种浪费,也没必要,还会导致代码变得复杂且难以理解。
  • 装饰者和被装饰对象有着相同的超类型。
  • 可以使用一个或者多个装饰者包装一个对象。
  • 既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象代替它。
  • 装饰者可以在所委托被装饰者的行为之前与之后,加上自己的行为,甚至将被装饰者的行为整个替换掉,以达到特定的目的。
  • 对象可以在任何时候被装饰,所以可以在运行时动态的、不限量的用你喜欢的装饰者来装饰对象。
  • 继承属于拓展形式之一,但不见得是达到弹性设计的最佳方式。
  • 组合和委托可用于在运动时动态的加上新的行为。
  • 除了继承,也可以使用像装饰者模式这样的链式扩展行为。
  • 装饰者模式意味着一群装饰者类,这些类用来包装具体组件。
  • 装饰者类反映出被装饰的组件类型。事实上,他们具有相同的类型,都经过接口或继承实现。
  • 理论上,可以用无数个装饰者包装一个组件。
  • 装饰者一般对组件的客户是透明的,除非客户程序依赖于组件的具体类型。

定义

装饰者模式动态地将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

  • 继承Component抽象类是为了有正确的类型,因为装饰者必须能取代被装饰者,而不是继承它的行为。在JAVA中可以使用接口,这里使用抽象类是因为装饰者模式通常使用抽象类。如果抽象类运行的好好的,还是别去修改它,
  • 将代码针对特定种类的具体组件做特殊的行为,会造成一旦用装饰者包装特定种类就会造成类型改变,进而导致程序出现问题。只有在针对抽象组件类型编程时,才不会因为装饰者而受到影响。
  • 装饰者该做的事就是增加行为到被包装对象上。当需要窥视装饰者链中每一个装饰者时,这就超出他们的能力了。只能每一次装饰时,将装饰信息传递,最后进行统一解析。

JAVA类中的一个应用

具体实现方法

  1. 创建组件抽象类,设置相应属性。这些属性会继承到之后的所有组件,并设置相应的getter和setter。需要在组件类中具体重写的方法设置为抽象方法。
  2. 创建装饰类的抽象类,将装饰类中需要进行重写的方法设置为抽象方法。
  3. 创建相应的装饰类,继承2中的装饰类的抽象类,设置一个组件抽象类的对象。用以接收装饰链时传入对象,并进行相应抽象方法的重写。
  4. 创建相应的组件类,继承组件抽象类,并重写抽象方法。
  5. 使用时,用组件抽象类类型变量new相应的装饰类或者组件类。一个对象进行装饰时,将哪一个对象依次传入每一个装饰器或组件中。

缺点

  • 会在设计中加入大量小类,使人不容易理解设计方式。

文章作者: 不二
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 不二 !
  目录