Ruby设计模式学习

一个好的编程语言应当全面地整合设计模式, 使它们消亡在代码中

Posted on February 19, 2015

前言

  • 一个好的编程语言应当全面地整合设计模式, 使它们消亡在代码中, 如c++使得c中的面向对象设计模式消亡, ruby中一句话的设计模式
  • ruby 特点:动态语言; 内建代码闭包, 类是真正的对象, 优雅的代码重用系统
  • ruby的特点使得代码变得可压缩, 是DRY的应用
  • ruby使得你把注意力放在解决实际问题上而不是其他工作中(如迭代器)

第一章 使用设计模式创建更好的程序

  • 软件设计的复杂, 很容易让人忽视重复出现的难点和解决方案的模式
  • 把变的和不变的分离
  • 针对接口编程, 而不是对实现编程
  • 组合优先于继承 继承的本意: 力图将父类和子类结合(高耦合); 任何超类中没有被仔细隐藏的内部操作都会在子类中曝光 组合: 低耦合, 更多的可能; 更好的封装性
  • 委托 获得继承的主要优势, 消除继承的副作用, 更大的灵活性 额外方法调用的性能损耗, 可能需要无聊的代理方法

  • 你不会用到它(YAGNI)

第三章 使用模板方法变换算法

应用场景: 需要变换算法时. 将不变的在基类中(模板方法), 将变换封装到其下的各个子类中, 基类可以提供一些方法的默认实现(钩子方法), 子类可以根据自己需求选择使用默认实现或者覆盖

  • 简单基于继承
  • 模板方法: 父类实现的处理高端流程的具体方法, 子类不进行覆盖, 子类完成其中的具体任务
  • 子类的共性是具有代码片段化
  • 钩子方法 父类中定义的可被子类重载的非抽象方法, 子类可以:
    • 重载实现不同的任务
    • 简单接收默认值

第四章 使用策略模式替换算法

应用场景: 把算法提取出来放到独立的对象中

  • 基于组合和委托
  • 所有策略类对象都有相同的接口
  • 模板模式和策略模式比较:
    • 都用于算法变换
    • 模板方法模式使用继承, 策略模式使用组合
  • 环境对象和策略对象数据共享的选择:
    • 环境对象向策略对象传递具体数据: 优点数据划分清晰, 缺点可能需要传递较多数据
    • 环境对象向策略对象传递环境对象自身: 优点简化了数据流动, 缺点增加了二者的耦合
  • ruby 通过传递代码块可以起到传递策略的效果: 好处是不需要为策略创建特殊的策略类, 局限是只限于简单的接口, 因为代码块只有公共接口call

第五章 通过观察者保持协调

  • 主题类(Subject) 引发变更消息的类, 通用接口: add_observer, delete_observer, notify_observers
  • 观察器(Observer) 想获得变更消息的类
  • ruby 标准库Observable: TODO
    • 使用: require 'observer' 类中include Observer
    • 在调用 notify_observers前需要调用changed 指明对象已经被修改, 否则notify_observers不会有效果
  • 可以使用代码块作为观察器, 好处是无需单独创建观察者类, ruby标准库Observable并不支持
  • 存在两种通知方式:
    • 观察者: observer.update(self)
    • 主题: observer.update(self, old_salary, new_salary) 或者 observer.update_salary(self, old_salary, new_salary)
  • 注意事项:
    • 需要注意通知观察者的频率和时机
    • 等主题对象进入稳定状态后再通知观察者
  • ActiveRecord::Observer TODO

第六章 使用组合模式将各部分组成整体

应用场景: 整体和部门有相似行为(支持相同的接口), 适用于需要一个对象继承链或者对象树

  • Component: 组件, 基类, 定义叶子类(部门)和组合类(整体)都必须支持的接口
  • Leaf: 叶子类, 代表最简单的任务或者部门
  • Composite: 组合类, 是leaf的组合 除了通用接口外, 还需要支持管理子任务的接口, 如add_sub_taskremove_sub_task, 并维护子任务的记录如@sub_tasks 管理子任务的接口可以提取到Component和Composite之间的一个类中, 如CompositeTask 管理子任务的接口还可以包括一些迭代操作运算方法, 如<< []等, 也可以mixin Enumerable实现迭代
  • 思考: 叶子对象和组合对象的区别是是否需要处理子节点, 但是有些场景的子节点会动态发展为组合类, 这种情况是否可以完全去掉Leaf类, 都使用Composite, 在add_sub_taskremove_sub_task去判断是否存在子任务

    当然如果能确定子节点不会发展为组合节点, Leaf还是有存在的必要性

  • 此模式中, 组合节点可以方便的遍历所有叶子节点, 但是叶子节点没法回溯, 可以给叶子节点增加parent方法, 在组合对象add_sub_task中设置子节点的parent

第七章 通过迭代器遍历集合

应用场景: 为外部世界提供访问一组聚合对象的途径, 外部世界无需掌握各个对象的聚合方式和排序方法

  • 外部迭代器: ‘外部’指迭代器是与聚合对象分开的独立对象
  • 内部迭代器: 所有迭代都发生在聚合对象内部
  • 内外比较:
    • 外部: 1)客户端控制迭代流程; 2) 外部迭代器方便共享; 实例: 两个已排序的数组进行合并排序(客户端需要控制迭代流程)
    • 内部: 1)客户端控制权较少; 2)优势在于简单和代码高清晰度
  • 混入Enumerable:
    • 实现each方法
    • 每个元素实现<=>方法

第八章 使用命令模式完成任务

应用场景: 将要做什么事情(不变)和如何做这个事情(多变)分离, Client 持有Command对象, 要做什么事情时调用Command对象的通用接口(委托), Command可以在运行时更换

  • Command:
    • 有共同接口的类簇, 用于封装不同命令(如何完成具体命令); 场景: 很多状态, 具备多个方法
    • 可以有另一个选择: 代码块(同样可以代表如何完成具体命令); 场景: 简单直接的命令
  • 实际应用:
    • 命令模式+组合模式 实现记录操作命令队列
    • 支持撤销的命令模式: Command支持执行和撤销2个接口, 如execute unexecute
  • 思考: 命令模式和策略模式的比较, 批量命令模式和组合模式的比较

第九章 使用适配器填补空隙

属于系列模式(一个对象包含在另一个对象中)

  • Client 对Target有一定了解(知道有哪些接口)
  • Target 代表Client期望的模式接口
  • Adapter 代表一个具体的适配器
  • Adaptee 被适配对象
  • ruby 中进行适配, 除了按照常规的开发一个Adapter外, 还有其他选择: 修改
    • 修改Adaptee, ruby 可以Open Class
    • 修改具体adaptee对象, 如单件方法

第十章 通过代理来到对象面前

代理管理对主题的访问, 系列模式

应用场景:

  • 保护代理: 控制对象访问, 方便将控制逻辑和核心逻辑分离, 也可以切换控制逻辑
  • 远程代理: 提供和位置无关的对象访问途径, 将非核心和核心逻辑分离. RPC系统工作原理
  • 虚拟代理: 延迟对象创建, 真正使用到时才进行创建真实对象

  • 代理对象(ServiceProxy)存在被代理对象(RealService, 也叫主题)的引用, 并且二者有同样的接口
  • ruby中解决代理苦差事(即代理要实现主题所有的相同接口): 采用method_missing + 消息发送(send)

第十一章 使用装饰者改善对象

将功能叠加到基本对象上, 系列模式, 用于可累加的有通用接口的附加功能的动态组合

  • 对于多种功能组合的实现选择:
    • 采用继承: 要求我们在设计时就考虑所有的组合可能
    • 采用组合: 可以在运行时建立需要的组合功能
  • Component: 压缩模块和装饰者的共同父类, 定义了通用接口
  • ConcreteComponent(压缩模块): 实现了基本模块功能的对象, 是装饰链的起点
  • Decorator: 装饰者, 包含一个Component的引用, 在递归调用通用接口时, 可以进行自定义预处理
  • 压缩模块和装饰者的比较:
    • 压缩模板实现了基本功能, 装饰者实现了扩展功能
    • 都可以作为被装饰者
  • 减少Component中的模板方法: ActiveRecord中提供了delegate (TODO)
  • ruby中一种装饰方法的实现: 在对象的单件类中采用alias重写需要附加功能的方法; 另一种是对对象extend需要的扩展方法
  • alias_method_chain old_method, new_funcion 目的是将原方法增加新的装饰功能, 实现:
    • 用户要创建old_method_with_new_function方法, 实现组合新功能, 新功能如果要使用旧功能采用old_method_without_new_function
    • old_method改名为old_method_without_new_function
    • old_method_with_new_function 改为 old_method

第十二章 使用单例确保只有一个

  • 通用的实现方式是将初始化方法声明为private, 将单例存储与类变量中
  • ruby 提供的singleton模块可以方便实现单例模式, 包括: 创建类变量, 初始化单例, 类方法instance, 将new 设为私有
  • 单例模式细分为:
    • 勤单例: 类加载时创建单例
    • 惰单例: 类方法instance 初次调用时创建单例
  • 其他单例实现:
    • 全局变量
    • class作为单例
    • module作为单例

第十三章 使用工厂模式挑选正确的类

应用场景: 把使用哪个类的决定放到子类中, 通常用于几个不同并相互关联的类进行选择的场景

  • 父类Creator(不变逻辑)和子类ConcreteCreator(变化逻辑), 类似模板方法模式(模板方法是创造product的factory_method方法)
  • ConcreteCreator和 Product子类是紧耦合
  • 参数化工厂方法用于实现一个工厂要生产多种类产品的情况
  • 参数化工厂创建产品的自由在Client, 不能确保多个产品共存冲突问题, 抽象工厂可以解决此问题

  • 抽象工厂用于创建共存对象场景, 整个抽象工厂就是一个策略簇, 各个抽象工厂子类实现了对象创建与组合的策略

  • 工厂模式: 模板方法在创建对象上的应用
  • 抽象工厂: 策略方法在创建对象上的应用

第十四章 通过生成器简化对象创建

TODO