大话设计模式 读书笔记

第1章 代码无错就是优? -- 简单工厂模式

实例程序: 计算器代码.

初学者问题:

命名规范, 条件判断, 异常情况处理.

面向对象编程:

通过封装, 继承, 多态把程序的耦合度降低.
容易维护, 扩展, 复用.

举例: 活字印刷.

改动程序:

  • 封装业务逻辑, 与界面分开.
  • 用继承和多态, 分离各种运算, 使程序可以灵活修改和扩展(增加开根运算).
  • 简单工厂模式: 处理实例化逻辑.

UML类图

第2章 商场促销 -- 策略模式

实例程序: 商场收银软件.

  • 基本需求: 根据商品单价和数量, 计算费用.
  • 扩展需求: 增加不同的打折和满减优惠活动.

简单工厂实现

相同属性和功能的对象的抽象集合才是类, 所以不用为每一种折扣写一个子类.
收费抽象类有三个子类: 正常收费; 打折收费; 返利收费.

缺点: 由于工厂本身包括了所有的收费方式, 商场是可能经常性地更改打折额度和返利额度, 每次维护或扩展收费方式都要改动这个工厂, 以致代码需要重新编译部署, 所以它不是最好的方法.

策略模式 Strategy

策略模式(Strategy): 它定义了算法家族, 分别封装起来, 让它们之间可以互相替换, 此模式让算法的变化, 不会影响到使用算法的客户.

实现: 增加一个Context类, 维护一个对Strategy对象的引用. 这个Context类被客户端持有.

进一步优化: 策略模式与简单工厂结合. 将创建具体Strategy的过程移入Context类.

策略模式的优点

  • 减少了各种算法类与使用算法类之间的耦合.
  • 继承有助于析取一系列算法中的公共功能.
  • 每个算法有自己的类, 可以单独做单元测试.
  • 算法修改独立.
  • 消除客户端条件语句, 避免了大量判断. (Context中仍然有switch语句, 进一步改进: 反射. 后续章节会讲.)

第3章 拍摄UFO -- 单一职责原则

场景: 散步偶遇天空中的不明飞行物, 用手机拍摄后拿回家看不清. 手机不如专业的相机.

单一职责原则

单一职责原则(SRP): 就一个类而言, 应该仅有一个引起它变化的原因.

如果一个类承担的职责过多, 就等于把这些职责耦合在一起, 一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力. 这种耦合会导致脆弱的设计, 当变化发生时, 设计会遭受到意想不到的破坏.

俄罗斯方块游戏的设计

考虑在不同平台上的程序逻辑复用, 将游戏逻辑和界面表示部分分离.

方块可移动区域: 二维整型数组, 数组的值为是否存在方块的标志, 存在为1, 不存在为0.
碰撞和消层变为数值检测判断. 下落, 旋转, 移动等都是在做数组具体项的值的变化.

界面表示逻辑根据数据进行绘制, 根据键盘命令调用数组的相应方法进行改变.

第4章 考研求职两不误 -- 开放-封闭原则

场景: 考研失败, 没有找工作.
举例: 一国两制.

开放-封闭原则

开放-封闭原则, 是说软件实体(类, 模块, 函数等) 应该可以扩展, 但是不可修改.

对于扩展是开放的, 对于更改是封闭的.

面对需求, 对程序的改动是通过增加代码进行的, 而不是更改现有的代码.

应该仅仅对呈现出频繁变化的那部分做出抽象, 对于应用程序中每个部分都刻意地进行抽象同样不是一个好主意. 拒绝不成熟的抽象和抽象本身一样重要.

第5章 会修电脑不会修收音机? -- 依赖倒转原则

场景: MM电脑蓝屏死机, 电话遥控修电脑, 拆掉一根内存条.

PC易插拔, 不管哪一个部件出问题, 都可以在不影响别的部件的前提下进行修改或替换.
强内聚, 松耦合.

依赖倒转原则

  • 高层模块不应该依赖底层模块, 两个都应该依赖抽象.
  • 抽象不应该依赖于细节, 细节应该依赖于抽象.
  • 要针对接口编程, 不要针对实现编程.

如果高层模块和底层模块都依赖于抽象(接口或抽象类), 只要接口是稳定的, 那么无论是高层模块还是低层模块都可以很容易地被复用.

里氏代换原则

里氏代换原则(LSP): 子类型必须能够替换它们的父类型.

由于子类型的可替换性才使得使用父类类型的模块在无需修改的情况下就可以扩展.

修收音机

收音机就是典型的耦合过度, 各个部件相互依赖, 难以维护.

第6章 穿什么有这么重要? -- 装饰模式

代码: 搭配服饰的系统.

  • 增加装扮 -> 开放-封闭原则. -> 抽象类, 继承.
  • 把所需的功能按正确的顺序串联起来进行控制. -> 装饰模式.

装饰模式 Decorator

装饰模式(Decorator), 动态地给一个对象添加一些额外的职责, 就增加功能来说, 装饰模式比生成子类更为灵活.

装饰模式把每个要装饰的功能放在单独的类中, 并让这个类包装它所要装饰的对象, 因此, 当需要执行特殊行为时, 客户代码就可以在运行时根据需要有选择地, 按顺序地使用装饰功能包装对象了.

第7章 为别人做嫁衣 -- 代理模式

场景例子: 男生A想追求女生C, 但是他们并不认识, 于是A委托一个认识C的男生B, 向C送礼物.

程序设计: 两个男生实现了同样的接口, 接口中规定了送礼物的各种方法.
追求者包含要追求的女生对象和具体的送礼物方法, 而代理追求者包含追求者对象, 其方法实现调用追求者对象的对应方法.

代理模式 Proxy

代理模式(Proxy), 为其他对象提供一种代理以控制对这个对象的访问.

代理模式应用:

  • 远程代理: 为一个对象在不同的地址空间提供局部代表.
  • 虚拟代理: 根据需要创建开销很大的对象. 通过它来存放实例化需要很长时间的真实对象.
  • 安全代理: 用来控制真实对象访问时的权限.
  • 智能指引: 当调用真实对象时, 代理处理另外一些事.

第8章 雷锋依然在人间 -- 工厂方法模式

背景故事: 有个学雷锋做好事的同学病了, 请其他同学帮忙去孤寡老人家里干活.

计算器程序的两种实现:

  • 简单工厂模式实现: switch-case.
  • 工厂方法模式实现: 每种算法对应一个工厂.

简单工厂 vs. 工厂方法

简单工厂模式的最大优点在于工厂类中包含了必要的逻辑判断, 根据客户端的选择条件动态实例化相关的类, 对于客户端来说, 去除了与具体产品的依赖.

简单工厂的缺点: 对扩展和修改都开放, 违背了开-闭原则.

工厂方法模式 Factory Method

工厂方法模式(Factory Method), 定义一个用于创建对象的接口, 让子类决定实例化哪一个类. 工厂方法使一个类的实例化延迟到其子类.

符合开-闭原则, 却把选择逻辑放在了客户端.

以简单工厂和工厂方法模式实现学雷锋的例子. 工厂方法克服了简单工厂违反开-闭原则的缺点, 如果要更换对象, 不需要大的改动, 只要更换工厂就可以.

第9章 简历复印 -- 原型模式

程序需求: 写一个简历类, 可以设置基本信息和工作经历. 最终产生工作经历不同的多份简历.

问题: 三份简历需要三次实例化, 如何避免多次实例化?

原型模式 Prototype

原型模式(Prototype), 用原型实例指定创建对象的种类, 并且通过拷贝这些原型创建新的对象.

原型模式其实就是从一个对象再创建另外一个可定制的对象, 而且不需知道任何创建的细节.

.Net中提供了ICloneable接口.

一般在初始化的信息不发生变化的情况下, 克隆是最好的办法. 既隐藏了对象创建的细节, 又对性能是大大的提高.

浅复制与深复制

浅复制: 被复制对象的所有变量都含有与原来的对象相同的值, 而所有的对其他对象的引用都仍然指向原来的对象.

浅复制对于值类型没什么问题, 对于引用类型, 就只是复制了引用, 对引用的对象还是指向了原来的对象.

深复制把引用对象的变量指向复制过的新的对象, 而不是原有的被引用的对象.

第10章 考题抄错会做也白搭 -- 模板方法模式

程序例子: 学生抄题然后解答.

程序1: 学生分别抄题然后给出答案.
问题: 如果老师要改题, 就得修改多处, 而且也存在某些学生可能会抄错的问题.

改进1: 抽出父类, 处理抄题部分, 只有答题部分不同, 由子类完成.

改进2: 答题部分也有部分内容相同, 只有答案不同. -> 在基类中增加虚方法, 填写答案, 由子类覆写填答案.

模板方法模式

模板方法模式, 定义一个操作中的算法的骨架, 而将一些步骤延迟到子类中. 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤.

模板方法模式通过把不变行为搬移到超类, 去除子类中的重复代码.

第11章 无熟人难办事? -- 迪米特法则

场景: 小菜第一天上班, 人事处带他去IT部门找一个小张领电脑装电脑. 结果小张临时出去处理其他事情, 于是小菜就一直等, 而IT部的另外两个员工很闲却不帮忙, 因为单子上写的是小张负责. 于是小菜等了一天, 快结束时小张回来才帮他处理.

问题: 如果公司IT部门有主管, 或者有事直接是整个IT部门负责, 就不会出现具体到个人的资源分配问题. -> 面向接口编程.

迪米特法则

迪米特法则, 如果两个类不必彼此直接通信, 那么这两个类就不应当发生直接的相互作用. 如果其中一个类需要调用另一个类的某一个方法的话, 可以通过第三者转发这个调用.

在类的结构设计上, 每一个类都应当尽量降低成员的访问权限.

第12章 牛市股票还会亏钱? -- 外观模式

举例: 股民炒股, 投资多个股票, 自己不易掌握, 买基金, 由专业的经理人进行管理. -> 解除耦合, 外观模式, 又叫门面模式.

外观模式 Facade

外观模式(Facade), 为子系统中的一组接口提供一个一致的界面, 此模式定义了一个高层接口, 这个接口使得这一子系统更加容易使用.

何时使用外观模式:

  • 设计初期的逻辑分层.
  • 开发阶段增加外观提供简单接口.
  • 维护遗留系统.

第13章 好菜每回味不同 -- 建造者模式

场景: 大排档的饭, 一份好吃, 另一份忘了放盐. 中式快餐依赖于厨师.

提出问题: 为什么麦当劳肯德基的食物口吻稳定? -> 工作流程规范稳定.

程序需求: 画小人程序, 画不同的小人, 如何复用程序, 避免关键部分缺失? -> 建造者模式, 又叫生成器模式.

建造者模式 Builder

建造者模式(Builder), 将一个复杂对象的构建与它的表示分离, 使得同样的构建过程可以创建不同的表示.

  • Builder为创建一个Product对象的各个部件指定的抽象接口.
  • ConcreteBuilder是具体的建造者, 实现Builder接口, 构造和装配各个部件.
  • Director指挥者, 指挥建造过程, 使用ConcreteBuilder构建产品.

建造者模式是当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时适用的模式.

第14章 老板回来, 我不知道 -- 观察者模式

场景: 公司同事讨论股票, 如果老板回来, 秘书则打电话通知大家.

代码实现, 解耦1: 抽象的观察者; 解耦2: 抽象的通知者接口.

观察者模式

观察者模式又叫发布-订阅(Publish/Subscribe)模式.

观察者模式定义了一种一对多的依赖关系, 让多个观察者对象同时监听某一个主题对象. 这个主题对象在状态发生变化时, 会通知所有观察者对象, 使它们能够自动更新自己.

关键类: Subject, ConcreteSubject, Observer, ConcreteObserver.

观察者模式的不足: 有可能所有观察者对象是已经写好的控件, 它们没有办法实现同一个观察者接口; 每个具体的观察者, 有可能是不同的方法需要被调用.

事件委托实现

多个观察者没有共同的基类, 并且有各自不同名字的更新方法.
此时通知者无法遍历通知 -> 委托(.Net中的delegate).

首先声明一个委托类型EventHandler. 在通知者类中声明委托事件.
注册观察者时将观察者的更新方法挂钩到通知者的这个委托事件上. 在通知者通知时调用该委托事件.

委托就是一种引用方法的类型. 一旦为委托分配了方法, 委托将与该方法具有完全相同的行为. 委托可以看作是对函数的抽象, 是函数的"类", 委托的实例将代表一个具体的函数.

一个委托可以搭载多个方法, 所有方法被依次唤起.
前提: 委托对象所搭载的所有方法必须具有相同的参数列表和返回值类型.

场景: 有同学的手机丢了, 委托小菜给班级所有同学发个短信通知其他人该同学已经换号, 请大家更新号码.

第15章 就不能不换DB吗? -- 抽象工厂模式

场景: 公司有两个项目业务相同, 但是要用两种数据库.

例子代码存在的问题: 直接创建了具体的数据库操作类.

解决方法: 用工厂方法模式. 工厂方法模式定义一个用于创建对象的接口, 让子类决定实例化哪一个类.

代码改进1: 抽象出对数据库的一个表的访问接口, 解除与具体数据库访问的耦合.
并定义抽象工厂接口, 由具体的工厂实现实例化.

依然存在问题: 依然存在很多指明具体工厂的代码.

代码改进2: 增加了对数据库其他表的访问接口. 涉及到多个产品系列的问题 -> 抽象工厂模式.

抽象工厂模式 Abstract Factory

抽象工厂模式(Abstract Factory), 提供一个创建一系列相关或相互依赖对象的接口, 而无需指定它们具体的类.

优点:

  • 易于交换产品系列. 只需要改变一个具体的工厂.
  • 具体的创建实例过程与客户端分离, 客户端通过接口操纵实例.
    (开放-封闭原则, 倒转依赖原则).

缺点:

  • 增加功能时要改动的类比较多.
  • 实例化具体工厂的地方比较多, 替换实现时仍需要更改多处.

用简单工厂来改进抽象工厂

去掉了工厂类, 增加一个数据访问类, 其中用简单工厂模式来决定实例化哪些具体的数据库访问类.

缺点: 如果要增加一系列的实现, 需要在简单工厂类的每个方法switch中增加case.

用反射 + 抽象工厂的数据访问程序

解决思路1: 依赖注入框架.
解决思路2: 反射.

利用字符串来实例化对象, 而变量是可以更换的.

问题: 更改数据库还是需要去改程序(db名称)重新编译.
改进: 用配置文件来解决更改变量的问题.

第16章 无尽加班何时休 -- 状态模式

程序: 不同的时间对应不同的工作状态.

代码的坏味道: 方法过长, 有很多的判断分支.
违背了单一职责原则和开放-封闭原则.

状态模式 State

状态模式(State), 当一个对象的内在状态改变时允许改变其行为, 这个对象看起来像是改变了其类.

状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况. 把状态的判断逻辑转移到表示不同状态的一系列类当中, 可以把复杂的判断逻辑简化.

状态模式的好处是将与特定状态相关的行为局部化, 并且将不同状态的行为分割开来.

状态模式通过把各种状态转移逻辑分布到State的子类之间, 来减少相互间的依赖.

什么时候考虑使用状态模式?
当一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为时, 就可以考虑使用状态模式了.

第17章 在NBA我需要翻译 -- 适配器模式

背景: 姚明刚去NBA时需要翻译.

适配器模式 Adapter

适配器模式(Adapter), 将一个类的接口转换成客户希望的另外一个接口. Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作.

联想: 电源适配器, 翻译.

类适配器模式: 通过多重继承. (某些语言不支持.)
对象适配器模式: 利用包含.

何时使用适配器模式?
使用一个已经存在的类, 但如果它的接口, 也就是它的方法和你的要求不相同时, 就应该考虑用适配器模式.

通常是在开发后期或维护期再考虑使用适配器模式; 在设计初期, 应该尽量为类似的功能设计相同的接口, 或考虑通过重构统一接口.

在双方都不太容易修改时使用适配器模式: 比如使用第三方组件.

.Net中的DataAdapter.

扁鹊三兄弟医术的例子: 事后控制不如事中控制, 事中控制不如事前控制.
适配器模式是后期才考虑的一个模式.

第18章 如果再回到从前 -- 备忘录模式

背景: 游戏存档, 下棋悔棋, 文档中的撤销, 网页中的后退.

程序: 游戏角色的数据存档.
第一版程序的缺点: 游戏角色的细节暴露给了客户端, 保存和恢复都暴露了实现细节, 不好扩展和修改.

备忘录模式 Memento

备忘录(Memento): 在不破坏封装性的前提下, 捕获一个对象的内部状态, 并在该对象之外保存这个状态. 这样以后就可将该对象恢复到原先保存的状态.

  • Originator: 发起人, 负责创建一个备忘录Memento, 用以记录当前时刻它的内部状态, 并可使用备忘录恢复内部状态. Originator可根据需要决定Memento存储哪些内部状态.
  • Memento: 备忘录, 负责存储Originator对象的内部状态, 并可防止Originator以外的其他对象访问备忘录.
  • Caretaker: 管理者, 负责保存好备忘录(Memento), 不能对备忘录的内容进行操作或检查.

全部状态保存可以用clone的方式实现, 更多可能的情况是保存部分状态, 用备忘录.

适用情形:

  • 备忘录模式适用于功能比较复杂的, 需要维护或记录属性历史的类, 或者需要保存的属性只是众多属性中的一小部分时.
  • 使用命令模式时, 如果需要实现命令的撤销功能, 那么可以使用备忘录模式来存储可撤销操作的状态.
  • 一些对象的内部信息必须保存在对象以外的地方, 但是必须要由对象自己读取. 这时使用备忘录可以把复杂对象内部信息对其他的对象屏蔽起来.
  • 当角色状态改变的时候, 有可能这个状态无效, 这时候可以使用暂时存起来的备忘录将状态复原.

第19章 分公司 = 一部门 -- 组合模式

场景: 为一个有很多分公司的大公司写OA系统. 每个分公司, 办事处和总公司一样都有相应的人力, 财务等部门, 但系统要求总部和分公司不是平行的, 仍保持一定的树状结构.

分析: 整体与部分可以一致对待.
例子: 卖电脑的商家可以卖电脑和配件; 复制文件可以逐个文件复制也可以复制文件夹; 文本编辑, 单个文字和整段文字都可以更改样式.

组合模式 Composite

组合模式(Composite), 将对象组合成树形结构以表示'部分-整体'的层次结构, 组合模式使得用户对单个对象和组合对象的使用具有一致性.

Component: 组合中的对象声明接口.
Leaf: 叶子节点.
Composite: 有枝节点.

透明方式: Component中声明所有用来管理子对象的方法. 好处: 叶子节点和枝节点对外没有区别, 具备完全一致的行为接口. 问题: 叶子节点中的一些方法实现是没有意义的.

安全方式: Component中不去声明用于管理子对象的方法. 好处: 叶子节点不用实现这些方法. 问题: 不够透明, 树叶和树枝将不具有相同的接口, 客户端的调用需要做相应的判断.

何时使用组合模式:

  • 需求中是提现部分与整体的层次结构.
  • 希望用户可以忽略组合对象与单个对象的区别.

例子: 自定义控件.

第20章 想走? 可以! 先买票 -- 迭代器模式

场景: 公交汽车售票员对车厢里的所有人和大件行李进行遍历, 让大家买票.

迭代器模式 Iterator

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

当你需要访问一个聚集对象, 而且不管这些对象是什么都需要遍历的时候, 另外, 你需要对聚集对象有多种方式遍历时, 就应该考虑用迭代器模式.

迭代器模式为遍历不同的聚集结构提供如开始, 下一个, 是否结束, 当前哪一项等统一的接口.

很多高级编程语言已经吧这个模式做在语言中了.

第21章 有些类也需要计划生育 -- 单例模式

程序实例: 窗体程序, 希望工具箱窗体只出现一次.

简单解决方案1: 声明全局变量, 判断null.
缺点: 多个使用的地方会出现重复的判断代码. -> 把它们提炼到同一个地方.

改进: 由类自己控制实例化及判断.

  • 构造函数私有.
  • 静态变量.
  • 静态获取方法, 创建和访问唯一实例.

单例模式 Singleton

单例模式(Singleton), 保证一个类仅有一个实例, 并提供一个访问它的全局访问点.

多线程时的单例: lock加锁.
性能进一步改良: 双重锁定(Double-Check Locking).

静态初始化: sealed阻止派生; 变量标记为readonly.

第22章 手机软件何时统一 -- 桥接模式

场景: 不同手机品牌和不同的应用软件如何设计类.

两种复杂并且不合理的设计:

  • 手机品牌抽象类 <- 手机品牌具体类 <- 手机品牌下的各种应用类.
  • 手机软件抽象类 <- 手机软件具体类 <- 不同品牌版本的具体应用类.

用继承的缺点:

  • 对象的继承关系是在编译时就定义好了, 所以无法在运行时改变从父类继承的实现.
  • 子类的实现与父类有非常紧密的依赖关系, 以至于父类实现中的任何变化必然会导致子类发生变化.
  • 当你需要复用子类时, 如果继承下来的实现不适合解决新的问题, 则父类必须重写或被其他更适合的类替换.

继承这种依赖关系限制了灵活性并最终限制了复用性.
(不要拿着锤子看所有东西都成了钉子.)
一定要在是'is-a'的关系时再考虑使用继承.

合成/聚合复用原则

合成/聚合复用原则(CARP), 尽量使用合成/聚合, 尽量不要使用类继承.

合成(Composition, 也有翻译成组合)和聚合(Aggregation)都是关联的特殊种类.

  • 合成则是一种强的'拥有'关系, 体现了严格的部分和整体的关系, 部分和整体的生命周期一样.
  • 聚合表示一种弱的'拥有'关系, 体现的是A对象可以包含B对象, 但B对象不是A对象的一部分.

例子: 大雁和翅膀是合成关系; 大雁和雁群是聚合关系.

改进后的松耦合例子程序:

  • 手机品牌类 <- 具体手机品牌类.
  • 手机软件类 <- 具体软件类.
  • 手机品牌抽象类包含手机软件抽象类对象.

改进后增加软件和手机品牌都只需要增加新的类 -> 开放-封闭原则.

桥接模式 Bridge

桥接模式(Bridge), 将抽象部分与它的实现部分分离, 使它们都可以独立地变化.
这里的实现指的是抽象类和它的派生类用来实现自己的对象.

解释: 实现系统可能有多角度分类, 每一种分类都有可能变化, 那么就把这种多角度分离出来让它们独立变化, 减少它们之间的耦合.

第23章 烤羊肉串引来的思考 -- 命令模式

场景: 烤肉摊: 烤串人和客户之间紧密耦合, 比较混乱; 烤肉店: 由服务员记录客户请求并记录, 再通知烤肉师傅, 利于管理, 支持撤销.

松耦合设计: 定义抽象命令类, 其中包含烤肉师傅; 具体命令类中烤肉师傅执行具体行为; 服务员类包含命令对象, 通知方法通知命令执行.

进一步扩展: 在服务员类中: 增加盛放命令的容器; 对不合理命令进行回绝判断; 记录日志和取消订单; 遍历命令通知执行.

命令模式 Command

命令模式(Command), 将一个请求封装为一个对象, 从而使你可用不同的请求对客户进行参数化; 对请求排队或记录请求日志, 以及支持可撤销的操作.

命令模式的作用:

  • 能较容易地设计一个命令队列.
  • 可以较容易地将命令记入日志.
  • 允许接收请求的一方决定是否要否决请求.
  • 可以容易地实现对请求的撤销和重做.
  • 由于新加的具体命令类不影响其他类, 因此新加具体命令类很容易.
  • 把请求一个操作的对象与知道怎么执行一个操作的对象分割开.

第24章 加薪非要老总批? -- 职责链模式

背景: 小菜要求加薪, 向经理提出, 经理向总监提出, 总监向总经理提出.
程序: 申请处理程序, 申请(包含类别, 内容, 数量)提交给管理者, 每个管理者根据申请细节决定自己是否能够处理(比较长的方法, 多条分支).

职责链模式 Chain of Responsibility

职责链模式(Chain of Responsibility): 使多个对象都有机会处理请求, 从而避免请求的发送者和接收者之间的耦合关系. 将这些对象连成一条链, 并沿着这条链传递该请求, 直到有一个对象处理它为止.

当客户提交一个请求时, 请求是沿链传递直至有一个ConcreteHandler对象负责处理它.

接收者和发送者都没有对方的明确信息, 且链中的对象自己也并不知道链的结构. 结果是职责链可简化对象的相互连接, 它们仅需保持一个指向其后继者的引用, 而不需保持它所有的候选接受者的引用.

第25章 世界需要和平 -- 中介者模式

中介者模式又叫调停者模式.
举例: 联合国组织.

'迪米特法则', 如果两个类不必彼此直接通信, 那么这两个类就不应当发生直接的相互作用. 如果其中一个类需要调用另一个类的某一个方法的话, 可以通过第三者转发这个调用.

通过中介者对象, 可以将系统的网状结构变成以中介者为中心的星形结构, 每个具体对象不再通过直接的联系与另一个对象发生相互作用, 而是通过'中介者'对象与另一个对象发生相互作用.

之前'迪米特'法则的例子: 公司的IT部门管理, 由主管来协调工作.

中介者模式 Mediator

中介者模式(Mediator), 用一个中介对象来封装一系列的对象交互. 中介者使各对象不需要显式地相互引用, 从而使其耦合松散, 而且可以独立地改变它们之间的交互.

程序例子1: 不同的同事类之间通过中介者对象来通信.
程序例子2: 国家之间通过联合国安理会通信.

中介者模式优点:

  • Mediator的出现减少了各个Colleague的耦合, 使得可以独立改变和复用各个类.
  • 把对象协作进行了抽象, 将中介作为一个独立的概念并将其封装在一个对象中, 这样关注的对象就从对象各自本身的行为转移到它们之间的交互上来, 也就是站在一个更宏观的角度去看待系统.

中介者模式的缺点:

  • 具体中介类可能会变得非常复杂, 不容易维护.

中介者模式例子: Windows程序中的Form或Web网站程序的aspx.

中介者模式一般应用于一组对象以定义良好但是复杂的方式进行通信的场合, 以及想定制一个分布在多个类中的行为, 而又不想生成太多的子类的场合.

第26章 项目多也别傻做 -- 享元模式

背景: 小菜接了多个小型外包项目, 给私营业主做网站. 多个网站要求类似, 形式有一些变化.

问题: 真的需要每个网站都分别有一份代码, 每个网站租用一个虚拟空间吗?
它们本质上是一样的代码, 如果网站增多, 实例增多, 对服务器的资源浪费严重.

共享代码: 大型的博客网站, 电子商务网站, 里面的每一个博客或商家也可以理解为一个小的网站. -> 利用用户ID来区分不同的用户, 具体数据和模板可以不同, 但代码核心和数据库却是共享的. -> 节省服务器资源; 代码维护和扩展都更容易.

享元模式 Flyweight

享元模式(Flyweight), 运用共享技术有效地支持大量细粒度的对象.

  • FlyweightFactory: 创建并管理Flyweight对象. 它主要用来确保合理地共享Flyweight, 当用户请求一个Flyweight时, FlyweightFactory对象提供一个已创建的实例或者创建一个(如果不存在的话).
  • Flyweight: 所有具体享元类的超类或接口, 通过这个接口, Flyweight可以接受并作用于外部状态. 其子类并不强制共享.

网站共享代码: 同样类型的网站使用同一个实例, 减少实例个数.

问题: 网站的数据不同? -> 外部状态.

内部状态与外部状态

  • 内部状态: 在享元对象内部并且不会随环境改变而改变的共享部分.
  • 外部状态: 随环境改变而改变的, 不可以共享的状态.

享元模式可以避免大量非常相似类的开销. 在程序设计中, 有时需要生成大量细粒度的类实例来表示数据. 如果能发现这些实例除了几个参数外基本上都是相同的, 有时就能够大幅度地减少需要实例化的类的数量. 如果能把那些参数移到类实例的外面, 在方法调用时将它们传递进来, 就可以通过共享大幅度地减少单个实例的数目.

享元模式应用

  • 如果一个应用程序使用了大量的对象, 而大量的这些对象造成了很大的存储开销.
  • 对象的大多数状态可以是外部状态, 如果删除对象的外部状态, 那么可以用相对较少的共享对象取代很多组对象, 此时可以考虑使用享元模式.

应用: .Net中的string: 相同的字符串常量其实是共享内存的, 所以引用相同.

围棋, 五子棋, 跳棋等: 颜色是内部状态, 位置是外部状态.

第27章 其实你不懂老板的心 -- 解释器模式

背景: 从老板谈话中听出弦外之音.

解释器模式 Interpreter

解释器模式(Interpreter), 给定一个语言, 定义它的文法的一种表示, 并定义一个解释器, 这个解释器使用该表示来解释语言中的句子.

解释器模式需要解决的是, 如果一种特定类型的问题发生的频率足够高, 那么可能就值得将该问题的各个实例表述为一个简单语言中的句子. 这样就可以构建一个解释器, 该解释器通过解释这些句子来解决该问题.

比如:

  • 匹配字符串 -> 正则表达式.
  • 浏览器解释HTML文法, 显示网页.
  • 机器人指令.

当有一个语言需要解释执行, 并且你可以将该语言中的句子表示为一个抽象语法树时, 可使用解释器模式.

好处: 可以很容易地改变和扩展文法, 因为该模式使用类来表示文法规则, 你可使用继承来改变或扩展该文法. 也比较容易实现文法, 因为定义抽象语法树中各个节点的类的实现大体类似, 这些类都易于直接编写.

不足: 解释器模式为文法中的每一条规则至少定义了一个类, 因此包含许多规则的文法可能难以维护和管理. 建议当文法非常复杂时, 使用其他的技术如语法分析程序或编译器生成器来处理.

程序实例: 音乐解释器.

第28章 男人和女人 -- 访问者模式

背景: 写一个程序, 输出男人和女人在相同状态下的不同反应, 比如: 男人成功时xxx; 女人成功时yyy.

关键点:

  • 人分为男人和女人两类, 这个分类是稳定的. 所以可以在抽象的状态类中, 声明男人反应和女人反应两个方法. 这样方法个数是稳定的, 不会很容易地发生变化.
  • 而人的抽象类中有一个抽象方法接受状态对象.
  • 客户程序中将具体程序作为参数传递给具体的人的类. (第一次分派.)
  • 具体的男人或女人的类在接受方法中调用状态对象中对应自己的那个方法, 并将自己作为参数传递进去. (第二次分派.)

访问者模式 Visitor

访问者模式(Visitor), 表示一个作用于某对象结构中的各元素的操作. 它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作.

访问者模式适用于数据结构相对稳定的系统. 它把数据结构和作用于结构上的操作之间的耦合解脱开, 使得操作集合可以相对自由地演化.

访问者模式的优点就是增加新的操作很容易, 因为增加新的操作就意味着增加一个新的访问者. 访问者模式将有关的行为集中到一个访问者对象中.

访问者模式的缺点是使增加新的数据结构变得困难了.

posted @ 2018-07-17 08:55  圣骑士wind  阅读(924)  评论(0编辑  收藏  举报