每日分享 – 《Head First 设计模式》笔记

软件开发的一个不变真理:不管当初软件设计得多好,一段时间后,总是需要成长与改变,否则软件就会“死亡”。

1 OO 原则

虽然原则提供了方针,但在采用原则之前,必须全盘考虑所有的因素。

  1. 封装变化。
    • 把会变化的部分取出并封装起来,以便以后可以轻易地改动或扩充此部分,而不影响不需要变化的其它部分。
  2. 多用组合,少用继承。
    • 使用组合建立系统具有很大弹性,不仅可将算法族封装成类,更可以“在运行时动态地改变行为”,只要组合的行为对象符合正确的接口标准即可。
  3. 针对接口编程,不针对实现编程。
    • 指“针对超类编程”,意味着声明类时不用理会以后执行时的真正对象类型。
  4. 为交互对象之间的松耦合设计而努力。
    • 松耦合的设计之所以能让我们建立有弹性的 OO 系统,能够应对变化,是因为对象之间的互相依赖降到了最低。
  5. 类应该对扩展开放,对修改关闭。
    • 我们的目标是允许类容易扩展,在不修改代码的情况下,就可搭配新的行为。这样的设计具有弹性,可以应对改变,可以接受新的功能来应对改变的需求。
    • 每个地方都采用开放-关闭原则,是一种浪费,也没有必要,还会导致代码变得复杂且难以理解。
  6. 依赖抽象,不要依赖具体类。
    • 不能让高层组件依赖底层组件,而且不管高层或底层组件,“两者”都应该依赖于抽象。
  7. 最少知识原则:只和朋友交谈。
    • 在设计中,不要让太多的类耦合在一起,免得修改系统中的一部分,会影响到其它部分。
    • 会导致更多的“包装”类被制造出来,以处理和其它组件的沟通。
  8. 别找我,我会找你。
    • 将决策权放在高层模块中,以便决定如何以及合适调用低层模块。
    • 当高层组件依赖底层组件,而底层组件又依赖高层组件是,依赖腐败就会发生。换句话说,高层组件对待底层组件的方式是“别调用我们,我们会调用你”。
  9. 类应该只有一个改变的理由。
    • 类的每个责任都有改变的潜在区域。超过一个责任,意味着超过一个改变的区域。应尽量让每个类保持单一责任。
    • 区分设计中的责任是最困难的事情之一。我们的大脑习惯看着一大群的行为,然后将它们集中在一起,尽管它们可能属于多个不同的责任。想要成功的唯一方法,就是努力不懈地检查你的设计,随着系统的增长,随时观察没有迹象显示某个类改变的原因超出一个。

2 OO 模式

  1. 策略:封装可以互换的行为,并使用委托来决定要使用哪一个。
    • 定义:定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
  2. 观察者:让对象能够在状态改变时被通知。
    • 定义:定义了对象之间的一对多的依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
    • 让主题和观察者之间松耦合。
    • 推:推的方式被认为更“正确”。但观察者可能会被强迫收到一堆数据。
    • 拉:如果主题需要增加更多的状态,不用修改和更新对每个观察者的调用,只需要改变自己允许更多的 getter 方法来取得新增的状态。但观察者可能需要调用多次才能收集全所需要的状态。
  3. 装饰者:包装一个对象,以提供新对行为。
    • 定义:动态地将责任附加到对象上,若要扩展功能,装饰者提供比继承更有弹性的替代方案。
    • 维持了开放-封闭原则,但会造成设计中有大量的小类,如果过度使用,会让程序变得复杂。
  4. 工厂方法:由子类决定要创建的具体类是哪一个。
    • 定义:定义了一个创建对象的接口,但由子类决定要实例化的是哪一个。工厂方法让类把实例化推迟到子类。
    • 简单工厂:不是真正的设计模式,但可以将客户程序从具体类解耦。
  5. 抽象工厂:允许客户创建对象的家族,而无需指定他们的具体类。
    • 定义:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
    • 客户不需要知道实际产出的具体产品是什么,从而将客户从具体的产品中被解耦。
    • 工厂方法 vs 抽象工厂:
      • 将应用程序从特定实现中解耦的方式不同,工厂方法用的是继承。
      • 抽象工厂用的是组合。另外,抽象工厂可以把一群相关的产品集合起来。
  6. 单例:确保有且只有一个对象被创建。
    • 定义:确保一个类只有一个实例,并提供一个全局访问点。
    • 全局变量的缺点:如果创建对象非常耗费资源,而后续没有用到,会形成浪费。而单例模式可以延迟类的实例化。
    • 为了解决多线程问题,方式 1:使用“急切”创建实例,而不用延迟实例化的做法。方式 2:使用“双重检查加锁”方式,检查实例,如果不存在则进入同步区块。只有第一次需要进入同步区块,代价较低。
    • 如果有多个类加载器,可能会导致多个单件并存。最好不要继承单件,因为构造方法是一般是私有的。
  7. 命令:封装请求成为对象。
    • 定义:将“请求”封装成对象,以便使用不同的请求、队列或日志来参数化其它对象。命令模式也支持可撤销的操作。
    • 命令模式将发出请求的对象和只需请求的对象解耦。被解耦的两者之间通过命令对象进行沟通。命令对象封装了接收者和一个或一组动作。
    • 实际操作时,常用“聪明”命令对象,也就是直接实现了请求,而不是将工作委托给接收者。
  8. 适配器:封装此对象,并提供不同的接口。
    • 定义:将一个类的接口转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间。
    • 对象适配器使用组合来适配被适配者,而类适配器是继承被适配者和目标类。
    • 适配器 VS 装饰者:
      • 装饰者的意图是扩展包装对象的行为或责任。
      • 适配器的意图是进行接口的转换。
  9. 外观:简化一群类的接口。
    • 定义:提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。
    • 外观不只是简化了接口,也将客户从组件的子系统中结偶。
    • 外观 VS 适配器:
      • 外观的意图是简化接口。
      • 适配器的意图是将接口转换成不同的接口。
  10. 模版方法:由子类决定如何实现一个算法中的步骤。
    • 定义:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模版方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
    • 模版方法 VS 策略:
      • 模版方法使用继承进行算法的实现。模版方法对算法有更多的控制权,算法的每一部分基本相同。重复使用的代码都被放到超类中,让所有的子类共享。
      • 策略通过对象组合的方式让客户选择算法实现。策略使用对象的组合,更具有弹性,可以在运行时改变算法。
    • 模版方法 VS 工厂方法:
      • 工厂方法是模版方法的一种特殊版本。
  11. 迭代器:在对象集合之中游走,而不暴露集合的实现。
    • 定义:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
    • 迭代器将遍历聚合的工作封装进一个对象中。
  12. 组合:客户用一致的方式处理对象集合和单个对象。
    • 定义:允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。
    • 使用组合结构,我们能把相同的操作应用在组合和个别对象上。换句话说,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。
    • 组合以单一责任原则换取透明性。
  13. 状态:封装了基于状态对行为,并使用委托在行为之间切换。
    • 定义:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
    • 将状态封装成独立的类,并将动作委托到代表当前状态的对象。
    • 状态 VS 策略:
      • 状态中,利用许多不同的状态对象。“改变行为”是建立在方案中的。
      • 策略中,不鼓励对象用于一组定义良好的状态转换。事实上,通常会控制对象使用什么策略。
  14. 代理:包装对象,以控制对此对象的访问。
    • 定义:为另一个对象提供一个替身或占位符以控制对这个对象的访问。
    • 使用代理模式创建代表对象,让代表对象控制某对象的访问,被代理的对象可以是远程的对象,创建开销大的对象或需要安全控制的对象。
    • 代理 VS 装饰者:两者的目的不同。
      • 代理代表对象,控制对象的访问。
      • 装饰者装饰对象,增加新的行为。

参考

  1. Eric Freeman, Elisabeth Freeman, Kathy Sierra and Bert Bates. Head First 设计模式 M. 北京:中国电力出版社. 2010.
正文完