设计模式

456 阅读19分钟

引言

程序由对象组成

- 对象
    - 数据
    - 操作

客户发起请求 - 通知到对象 - 对象操作 - 改变对象数据

难点

  • 系统解构
  • 逻辑调度
  • 应用模型
  • 分层标准

方式

  • 通过问题,动词名词,设计类操作
  • 系统协作和职责
  • 现实世界建模

设计模式(重要)

  • 设计模式帮你确定并不明显的抽象和描述这些抽象的对象

多态

  • 动态绑定允许你在运行时刻彼此替换有相同接口的对象,这种可替换性称为多态
  • 面向接口编程

  • 抽象类:为子类定义公共接口
  • 混入类:给其他类提供可选择的接口

复用机制

  • 继承:白箱复用,破坏了父类的封装性
  • 组合:黑箱复用,面向接口,优先使用
  • 委托:属于组合,使组合具有与继承同样的能力,如策略,状态
  • 参数化:curry?

对象结构

  • 聚合:一个对象拥有另一个对象或为其负责
  • 相识:仅仅知道,关联

架构问题及解决 (重要)

  • 显式指定类创建:用工厂,原型解决
  • 为请求指定特殊操作依赖:用职责链,命令模式
  • 对硬件或软件平台依赖: 用桥接模式
  • 对对象表示或实现依赖:表示,保存,定位,实现的客户连锁反应,用抽象工厂,桥接,代理,备忘录
  • 算法依赖:用建造者,迭代,策略,模板,访问者
  • 紧耦合:难以独立复用,用抽象工厂,命令,门面,备忘录,观察者,职责链
  • 子类继承过多:改用组合,委托
  • 类难以修改:适配器,装饰着,访问者

复杂场景

  • 应用程序: 减少依赖:独立封装,消除特定依赖: 不同上下文算法表示
  • 工具箱:强调复用,避免假设和依赖
  • 框架:一组相互协作的类 ,强调反向控制

设计模式概览

创建

  • abstract factory: 产品对象家族
  • builder: 创建组合对象
  • factory: 实例化子类
  • prototype: 实例化类
  • singleton:类唯一实例

结构

  • adapter: 兼容对象接口
  • bridge: 对象实现
  • composite: 对象的结构和组成
  • decorator:对象职责,不生成子类
  • flyweight: 对象存储开销
  • proxy: 如何访问一个对象,该对象位置
  • facade: 单个对象表示子系统

行为

  • chain of responsibility: 满足请求对象
  • command: 何时,怎样满足一个请求
  • interperter: 一个语言文化及解释
  • iterator: 遍历,访问对象
  • mediator: 对象间交互
  • memento:对象的信息存在在对象之外
  • observer: 多个对象依赖
  • state: 对象状态
  • stratgry: 算法
  • template: 算法中的步骤
  • vistor:作用于对象上的操作,但不修改这些对象

设计模式概览2

  • 创建:从无生有,生产创造。比如工厂模式。
  • 复制:从现有对象中创造新的对象。比如原型模式。
  • 委托:委托于能者,不费己心。比如适配模式。
  • 包装:在现有能力上添砖加瓦,或者以适当形式输出现有能力。比如装饰器模式、代理模式、命令模式。
  • 选择:用不同的策略来完成同一件事,各有优劣,视场景而选择。比如策略模式。
  • 组合:将多层次的具有同一能力的局部事物组合为整体。比如组合模式。
  • 聚合:将大量子能力和复杂交互组合成一个简洁的整体能力输出。比如外观模式、责任链模式。
  • 迁移:不同的状态对应不同的行为,在多种状态和行为之间相互转化。比如状态模式。
  • 共享: 共享大量原子对象和原子数据或共享资源和能力。比如享元模式、模板模式。
  • 同步: 将变化推送给关注者。比如观察者模式。
  • 访问: 提供一种内部访问或全局访问机制。比如迭代器模式、访问者模式、单例模式。
  • 存储: 存储历史状态。比如备忘者模式。
  • 引用: 将一种抽象和接口引向一种实现。比如桥接模式。
  • 中介: 在多种行为的复杂交互之间充当中间协调者,简化交互关系。比如中介者模式。

文档编辑器

  • 文档结构:图元:画自己,空间,父子 ,组合模式
    • 结构
    • 渲染
    • 同步位置
    • 不区分:统一操作
    • 区分: 特殊操作
  • 格式化:
    • composition:包含文档物理文档结构,调用compositor.compose
    • compositor:compose负责对每一个图元格式化
    • 策略模式:支持一系列格式化算法
  • 修饰用户界面
    • 装饰器:给对象增加职责,边界,滚动条
  • 多平台
    • 抽象工厂:确定平台
    • 工厂:由具体平台产出组件
    • 桥接:具体绘制方法
  • 用户操作
    • 功能请求与界面解耦:为每一个菜单一个适合他的command类,调用exec
    • 撤销重做:命令记录
  • 检查
    • 避免与物理结构耦合:后面可能还有其他功能叠加
    • 遍历接口:不同遍历方法:中序,后序,避免数字索引,用next,引入迭代器对象,提供了数组,列表等访问方式
    • 访问者模式:将分析与遍历分开,将分析分装在一个类中,把该类和合适的iterator结合起来,想对一个稳定的结构做不同的事,缺点是增加新的类型,访问者也要改变

创建型

abstract Factory

  • 当需要创建系列时
  • 每个系列,对应具体的工厂,创建具体的产品
abstractFactory{
    createProductA();
    createProductB()
}

ConcreteFactory1 {
    createProductA();
    createProductB()
}

AbstractProductA {}
ProductA1{}
ProductA2{}

builder

意图

  • 当创建复杂对象的算法应独立于该对象的组成部分以及他们的装配方式
  • 当构造过程必须允许被构造的对象有不同表示
director {
    consttuct() {
        for all objects in structue {
            builder.buildPart()
        }
    }
}

builder {
    buildPart()
}

concreteBuilder {
    buildPart() {
        return product
    }
}

factory

  • 当一个类不知道它所必须创建的对象的类
  • 当一个类希望子类来创建
  • 当类创建对象的职责委托给帮助类

creator {
    factoryMethod
    operation {
        product = factorymethod
    }
}

concreteCreator {
    factoryMethod {
        return new concreteProduct
    }
}

作用

  • 为子类提供挂钩,一种是没实现,一种是实现了一个基本的,子类可以覆盖
  • 链接平行的类层次(重要)
    • 当一个类将它的职责委托给独立类的时候产生
    • 比如对图形:伸展 移动,而这些信息不需要存在图形中,因此创建manipulator类,而不同图形会创建不同的maniputleor

prototype

  • 当系统独立于它的产品创建,构成
  • 要实例化的类在运行时刻指定
  • 避免创建许多类

我理解这里也吧功能聚合在了,应该在的类里,复用现有的实例,比如下面的例子,工具类直接使用图形类的实例

tool {
    manipulate()
}

rotateTool {
    manipulate() {
        p = prototype.clone()
        p.draw()
    }
}

graphic {
    draw()
    clone()
}

staffGraphic {
    draw()
    clone {
        return copy of this;
    }
}

注意点

  • 使用原型管理器:注册表(如rhino的配置)
  • 实现clone功能:js里天生支持

singleton

singleton {
    static instance() {
        return uniqueInstance
    }
    static uniqueInstance
}

总结

  • 创建子类,可能出现级联,创建太多
  • 对象复合:

结构型

adapter

target {
    request
}

Adapter {
    request() {
        adaptee.specifiRequest
    }
}

adaptee {
    specifiRequest
}

注意点:适配不起来

  • 窄接口
  • 增加代理对象,适配这个代理对象
  • 通过参数

bridge

当一个抽象可能有多个实现,用继承不灵活,继承使得抽象和实现固定在一起,难以对抽象和实现独立修改,继承的问题

  • 扩展不便:如多平台继承,意味着后面所有的子类,都需要继承
  • 与平台耦合:
window.imp = windowimp
window.drawRect = imp.drawLine();imp.DrawLine2();

注意点

  • 正确创建implementor:1.直接写逻辑,2.通过另一个直到细节的对象代理
  • 共享:在多个操作间共享,这个思路不错

composite

  • 想表示部分-整体的层次结构
  • 忽略组合对象与单个对象的不同
  • 常与职责链,decorateor一起使用
component {
    leaf:{
        operatin
    }
    composite:{
        [component, component, ...],
        add,
        remove,
        getchild
    }
}

decorator

动态给一个对象添加额外的职责,就增加功能来说,比生成子类更灵活 例:给用户界面添加一些特性,如边框,滚动,将组件嵌入另一对象中,由这个对象添加边框(高阶组件

borderDecorator -> scrollDeorator => textview'

decorator.component = component;
decorator.draw = function(){
    decorator.component.draw()
}
  • 处理可以撤销的职责
  • 由大量独立扩展,避免子类爆炸
  • 装饰从外部改变组件,组件无需对装饰有了解
  • 而策略模式中,组件本身需要对策略有了解
  • 适配器会给对对象全新的接口,装饰器不改变接口
  • 组合模式可以将装饰视为一个仅有一个组件的组合,但此模式重于聚合

facade

  • 使子系统间的通信依赖达到最小
  • 引入facade对象,为子系统中提供单一而简单的衔接
  • 构建层次的系统
外观类:提供编译系统子系统接口
compile {
    scaner
    parser
    paogameNodeBuilder
    codeGenerator
}

flyweight 享元

  • 使用大量对象,造成大开销
  • 对象大多数状态可变为外部状态
  • 内部状态储存在flyweight中
  • 外部状态由client对象存储或计算
  • 不对concreteFlyweight直接实例化,而交由工厂判断是否进行共享

例:文本编辑器,每一个字都是一个对象,太大,改为,26个字母对象,编辑器复杂引用编排,而编排等外部信息,放在外部的context上,这个context可以使btree,就是数据库,immable的概念

FlyweightFactory {
    if (flyweight[key] exits) {
        return exiting flyweight
    }
    else {
        create new flyeight
        add it to pool of flyweights
        return new flyweight
    }
}

proxy

比如 编辑图片,开销很大,我们建一个代理,只有真正绘制的时候才显示

document -> imageProxy -> image

proxy {
    realSubject:image
    draw: function() {
        if (image == 0) {
            image = loadImage()
        }
        image.draw
    }
}

  • 远程代理:不同空间地址局部代表
  • 虚代理:创建开销很大的对象
  • 保护代理:控制原始对象的访问
  • 智能指引:取代指针,访问对象执行一些操作
    • 记数
    • 第一次持久化,后面内存
    • 检查锁定
    • 转发请求

行为模式

职责链

给多个对象处理一个请求的机会,从而解耦发送者和接收者

  • 有多个对象可以处理一个请求,哪个对象处理由运行时自动确定
  • 在不明确接收者的情况下,向多个对象提交请求

实现

  • 后继者链
    • 定义新的: next: next.handle()
    • 复用对象解构,比如composite
  • 实现请求(手动)
    • 硬编码
    • 通过request派发
  • 自动派发
基类
base
constructor(HelpHandler) {
    this.HelpHandler = HelpHandler
}

实例
Dialog
handleHelp() {
    if (hasHelp()){
        
    }
    else {
        HelpHandler.handlerHelp()
    }
}

调用
appliaction = new Application()
dialog = new Dialog(application)
button = new Button(dialog);

command

  • 将请求分装成对象,解耦调用操作的对象和具有执行该操作所需信息的那个对象(实际执行者)
  • 增加界面设计灵活性,一个应用如果想让一个菜单与一个按钮代表同一功能,只需要共享command子类
  • 可以动态替换,用户实现上下文相关的菜单
  • 可以通过几个命令组成更大的命令来支持命令脚本

例子

menu -> menuItem(macroCommand.excute) -> macroCommand(循环子command执行excute) -> command
  • command模式是回调机制的一个面向对象的替代品
  • 不同时刻动态指定,排列和执行请求
  • 取消操作(unexecute),通过向前遍历操作历史列表
  • 支持修改日志
  • 支持事务,一个事务封装了对数据的一组变动,command提供了对事务进行建模

逻辑代码

command = new Command(receiver)
let useCommand = storeCommand(comand)
invoker.useCommand.Execute
command.action()
receiver.doSomething()
  • command智能程度
    • 直接调用receiver的方法
    • 没有receiver,自己实现所有功能
  • 取消重做,command上需要储存其他信息
    • receiver,真正执行处理请求的各操作
    • receiver 的操作参数
    • 如果改变了 receiver的其他值,那么这些值也要存起来,并且receiver需要提供恢复的方法
    • 多级取消,需要历史列表
    • 将可撤销的命令被放入历史列表前线拷贝下来,因为执行之后命令的状态可能会发生变化,比如删除,每次执行时,都不一样
  • 避免累积错误:有必要让command中存入更多信息,以保障对象可以比如精确的复原,可用memeto模式,让command访问信息而不暴露其他对象的内部信息

interpeter

如果一种特定类型的问题发生的频率足够高,那么可以将该问题描述为简单的语言,通过解释器去执行

  • NonetermainalExpression非终结表达式(如 AltrnationExpression, RepetitionExpression,SequenceExpression) 和 TerminalExpression 终结表达式(如:LiteraExpression)
  • 每一个非终结符表达式定义相应子表达式的解释操作,而各终结符表达式的解释操作构成了递归的基础
  • context:每一个节点的解释操作用上下文来存储和访问解释器的状态

实现

  • 创建抽象语法树
  • 定义解释操作:使用访问者,避免对类直接操作
  • flyweight模式共享终结符:终结符通常不储存关于它们在抽象语法树种位置的信息,在解释过程中,任何它们所需要的上下文信息都由父节点传递,因此在共享的状态和传入的区分的很明确

iterator

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

list {
    count
    append
    remove
}

listIterator {
    list: list,
    first
    next
    isDone
    current
}

filterListIterator {
    同一个对象,不同视角的迭代器
}

多态迭代 列表和聚合类之间,定好接口,降低耦合

AbstractList -> List
AbstractList -> List

Iterator -> ListIterator
Iterator -> SkipListterator

注意点

  • 谁控制迭代
    • 外部迭代:由client执行next,主动推进,灵活
    • 内部迭代:提交一个待执行的操作,方便
  • 谁定义遍历算法
    • 迭代器:方便在不同聚合上复用,但是会访问聚合的私有变量,破坏聚合的封装性
    • 聚合本身
  • 健壮保证:保证插入和删除操作不会干扰遍历,需要向聚合注册该迭代器,聚合要么挑战迭代器的状态,要么在内部维护额外的信息保证正确的遍历
  • 附加操作:previous,skipto

mediaor

比如表单组件联动:

- disable/enable
- 显示隐藏

中介者使组中的对象不再相互显示引用,这些对象仅知道中介者,从而减少相互连接的数目 不只是事件总线,还有这种更纯粹的方式

director {
    createWidget 创建组件
    widgetChange(widget) 组件改变
}

FontDirector {
    createWidget 创建组件(具体实现)
    widgetChange(widget) 组件改变 (具体实现)
}
    
widget {
    director
    change: director.widgetChange(this)
}

colleagueWidget {
    dosth:change()
}

效果

  • 减少子类生成:不同的场景,不用生成不同的组件,只需要生成meditator子类
  • 解耦colleague
  • 简化协议: 通过一对多的方式,简化多对多的方式
  • 对对象如何操作进行抽象:更容易理解系统
  • 控制集中化:它本身会变得复杂
  • 通信: 可以实现observer机制

memento

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

originator(原发器){
    setmemento: m.getstate()
    createMemto:new Memeto
}

memeto
{
    getstate()
    setstate()
}

caretaker(负责人,保存备忘录)
{
    memeto
}

负责人向原发器请求备忘录,保留一段时间后,将其送回原发器

  • 保持封装边界:避免暴露只应由原发器管理却又必须存储在原发器之外的信息
  • 简化原发器:同上,原发器就不用管这一块
  • 使用备忘录可能代价高
  • 存储增量式改变:历史列表定义特定的顺序,备忘录可以只储存一个命令所产生的增量改变而不是它所影响的每一个对象的完整状态

与command结合

movecommand {
    excute: {
        _state = solver.createMemto()
        target.move()
        solver.solve()
    }
    unexecute{
        target.move();
        solver.setmemnto(_state)
        solver.solve()
    }
}

solver{
    setmemto()
}

表示迭代状态的备忘录

collection {
    createInitalstate()
    next()
    isDone()
    currentItem()
    append()
    move()
    
}

observer

解决一对多的问题

subject(目标) {
    getstate();
    setstate();
    attacch
    detach
    notify {
        for of observers
        o.update
    }
}

Observer(观察者) {
    update(): {
        observerState =  subject.getstate();
    }
    observerState
}
  • 目标到观察者之间映射:可用关系链,hash表相互查找
  • 观察多对象:改造upate,把自己作为参数,让观察者知道是哪个目标发送
  • 谁出发更新
    • 改变对象操作后直接调notify,多个连续操作,效率低
    • client适当的时候调notify,比较复杂
  • 显示指定特定事件:上面这个一把梭,太粗暴
  • 封装复杂更新:更改管理器(changeManager)
    • 将目标映射到它的观察者提供接口(register(subject,observer)),不用直接引用
    • 定义特定更新策略:不一定全部更新,只更新适当

更改管理器

subject {
    attach: chman.register(this, o)
    detach
    notify: chman.notify
}

changeManger
{
    register(subject, observer)
    unregister(subject, observer)
    notify(){
        for s of subjects
            for o of s.observers
                    o.update(s)            
    }
    subject-Observer-Mapping
}

state

允许一个对象在其内部状态改变时改变他的行为,看起来似乎修改了他的类,比如:tcp连接状态

tcpconnection {
    tcpstate
    open: tcpstate.open
    close: tcpstate.close
    acknowLedge: tcpstate.acknowledge
}

TcpEstablishedState {
    open
    close
    acknodeledge
}

TcpListen {
    open
    close
    acknodeledge
}

适用性

  • 一个对象的行为取决于他的状态,并且必须在运行时根据状态改变它的行为
  • 一个操作中含有大量的分支语句,且这些分支依赖于该对象的状态,state模式将每一个条件分支放入独立的类中。

实现

  • 谁定义状态转换:
    • context自己
    • state类自身指定后继:更灵活合适,需要context增加接口
  • 基于表的另一种方法:使用表,将输入映射到状态转换,将条件代码映射为查找表(状态机)
    • 使用统一的表,使得转换准侧不够明确,难以理解
    • 难以加入伴随状态转换的一些动作。
    • state模式与状态相关的行为建模,而表驱动的方法着重于定义状态转换
  • 创建和销毁state对象
    • 上下文不经常改变:使用时创建
    • 经常改变:提前创建不销毁

已知应用 工具选择是画线还是画图

drawController {
    currentTool
    mousePressed()
    processKeyboard()
    initlaize
}

tool(基类) {
     handleMousePrese()
     handleMouseRelease()
     HandleCharacter()
     getCursor()
     active()
}

子类
creationTool{}
selectionTool{}
TextTool{}

表驱动方式

var fsm = new StateMachine({
    init: 'solid',
    transitions: [
      { name: 'melt',     from: 'solid',  to: 'liquid' },
      { name: 'freeze',   from: 'liquid', to: 'solid'  },
      { name: 'vaporize', from: 'liquid', to: 'gas'    },
      { name: 'condense', from: 'gas',    to: 'liquid' }
    ],
    methods: {
      onMelt:     function() { console.log('I melted')    },
      onFreeze:   function() { console.log('I froze')     },
      onVaporize: function() { console.log('I vaporized') },
      onCondense: function() { console.log('I condensed') }
    }
  });

strategy

许多类仅仅是“行为”上的差异,定义一系列的算法,把他们一个个封装起来,使他们可相互替换,使得算法可独立于使用它的客户而变化

动机

  • 直接包含算法,增加及系统复杂度
  • 不同时候需要不一样的算法,不想支持,不使用的算法
  • 算法难以分割,增加或者改变算法将十分困难

例子

compositon {
    repair: {
        compositor.compose()
    }
}

compositor {
    compose
}

simpleCompositor {compose}
texCompositor {compose}
arrayCompositor{compose}

模型

context {
    strategy: []
}

stragegy{}
concretsStrategyA {}
concretsStrategyB {}

效果

  • 替代继承的方法:不用每次都生成context子类
  • 消除条件:将条件判断委托给strategy对象

实现

  • 定义strategy和context接口:(两种比较经典的接口方式)
    • context将数据放在参数中
    • context自身作为一个参数传递给strategy
  • 模板:curry方式(比较特别,可以试试),提前声明好直接用

TEMPLATE 模板方法

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构,可重定义该算法的某些特定步骤, 形成了反向控制结构(由父类调用子类,而不是相反)

动机

  • application负责打开文档
  • 一旦文档从该文件读取后,由document对象表示
  • 其他场景可以继承Application和document,形成SpreadsheetApplication 和 SpreadsheetDocument子类
Application {
    openDocument: {
        if (!canOpenDocument) { return }
        doc = doCreateDocument()
        if (doc) {
            _docs.addDocument(doc)
            AboutToOpenDocument(doc)
            doc.open()
            doc.DoRead()
        }
    }
}

这个openDocument就是一个模板方法,用抽象操作定义算法,而子类将重新定义操作以提供具体的行为

适用

  • 一次性实现一个算法不变的部分,并将可变的行为留给子类实现
  • 各子类公共行为提取出来集中到公共父类中
  • 控制子类扩展,只在特定的hook操作
AbstractClass {
    templateMethod(){
        primitiveOperation1();
        primitiveOperation2
    }
    primitiveOperation1()
    primitiveOperation2()
}

ConcreteClass{
    primitiveOperation1()
    primitiveOperation2()
}

visitor

表示一个作用于某对象结构中的各元素的操作。使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作

nodeVisitor {
    visitAssignment(assignNode)
    VisitVariableRef(variableRefNode)
}

typeCheckingVisitor {
    visitAssignment(assignNode)
    VisitVariableRef(variableRefNode)
}
codeGeneratingVisitor {
    visitAssignment(assignNode)
    VisitVariableRef(variableRefNode)
}


Node{
    accept(NodeVisitor)
}

AssignmentNode {
    accept(nodeVisitor) {
        nodeVisitor.visitAssignment(this)
    }
}

迭代器与访问器

  • 迭代器可以通过调用节点对象的特定操作来遍历结构,不能对不同元素类型的对象结构操作
  • 访问器没有这个限制,它可以访问不具有相同父类的对象

实现

  • 双分派
    • 单分派:决定请求:请求的名,接收者的类型
    • 双分派:决定请求:请求的名,和2个接收者的类型,accpet是dobule-dispatch操作,它的含义决定:visitor类型和element类型,使得访问者可以对每一个类元素请求不同的操作
  • 谁来遍历:
    • 对象结构中
    • 访问者中
    • 独立迭代器对象中

与composite结合的例子

composite {
    accept(visitor) {
        for (iterator; i.isdone(); i.next) {
            i.currentItem.accpet(visitor)
        }
        vistor.vitstStm(this);
    }
}

行为模式总结

又回到程序的本质 客户发起请求 - 通知到对象 - 对象操作 - 改变对象数据

怎么了 谁来做 做什么 什么目的

封装变化

当一个程序某个方面经常发生变化是,需要拆解封装出来

  • stragy:算法
  • state: 状态相关行为
  • mediator: 对象间协议
  • iterator: 访问和遍历聚集对象的方法

2种对象

  • 分装该方面特征的新对象
  • 使用这些新对象的已有对象

对象作为参数

一些模式引入总是被做为参数对象

  • command
  • memoto

通信:聚集还是分布

  • mediator:集中
  • observer:分布

发送接受者解耦

  • command: 定义发送者和接受者之间的绑定关系
  • 观察者:定义通用接口
  • 中介者: 通过一个mediator对象间接互相引用
  • 职责链:沿一个潜在接受者链传递请求

混用

  • 职责链与template method
  • 职责链与command
  • iterpreter与state 定义语法分析上下文
  • 迭代器与访问者
  • composite 与 访问者

总结

模式级别而不是在类或对象级别上进行系统组装可以使我们更方便的获取同等的协同性