设计模式基础 - 《设计模式的艺术》读书笔记【1】

设计模式是我曾经想学但没学,后来不屑于学的技术。工作中遇到项目的重构不会设计模式就显得力不从心了,此次迟来的系统学习期望能带来实际工作中可以用到的启发。主要是阅读刘伟写的《设计模式的艺术:一本实例驱动的设计模式实践指南》1这本书。

设计模式(Design Pattern)是一套被反复使用的、多数人知晓的、经过分类编目的代码设计经验的总结,使用设计模式是为了可以重用代码,让代码更容易被他人理解并且提高代码的可靠性

狭义的设计模式是指GoF在《Design Patterns:Elements of Reusable Object-Oriented Software》一书中所介绍的23种经典设计模式。这23种设计模式可以分为三种。

  1. 创建型模式: 主要用于描述如何创建对象。
  2. 结构性模式: 主要用于描述如何实现类或对象的组合。
  3. 行为型模式: 主要用于描述类或对象怎样交互以及分配职责。

此外,根据某个模式是用于处理类之间的关系还是处理对象之间的关系,还可以分为类模式和对象模式。对某个设计模式可以用以上两种分类方式区分,例如单例模式是对象创建型模式,模版方法模式是类行为模式。

设计模式
设计模式

模式从不保证任何东西,它不能保证你一定能够做出可复用的软件,提高你的生产率,更不能保证世界和平。模式并不能替代人来完成软件系统的创造,它们只不过会给那些缺乏经验但却具备才能和创造力的人带来希望。

by: John Vlissides的著作Pattern Hatching Design Patterns Applied(《设计模式沉思录》)

如下图,定义名为Employee类,包含成员变量name、age、email,以及方法void modifyInfo();

类的UML图示

类的成员变量表示格式为: 可见性 名称: 类型[ = 默认值]

  • 可见性: 属性的可见性,包括公有(public)表示为+,私有(private)表示为-,保护(protected)表示为#
  • 名称: 表示成员变量名。
  • 类型: 表示成员变量类型。
  • 默认值: 成员变量初始值,可省略。

类的方法表示方式为: 可见性 名称([参数列表])[: 返回类型]

  • 可见性: 同成员变量定义。
  • 名称: 方法名。
  • 参数列表: 方法的参数,可省略。
  • 返回类型: 方法的返回值类型,可省略。

接口的UML表示与class有些区别,在接口中通常没有成员变量,只有方法的声明没有方法的实现。如下图,接口右上方有特殊的标记。

泛化关系UML图示

关联(Association)关系表示一类对象与另一类对象之间有关系。通常为一个类的对象作为另一个类的成员变量,UML类图中用实线连接两个类

关联关系UML图示

关联关系有如下几种形式。

  1. 双向关联: 双方都拥有对方类对象的成员变量。 双向关联关系UML图示

  2. 单向关联: 单方面拥有另一个类对象的成员变量。 单向关联关系UML图示

  3. 多重关联: 两个关联类在拥有对方的类成员数量上有对应关系。用如下图方式表示数量对应关系。

    多重性表示方式
    多重性表示方式

    例如,一个界面Form可以拥有0..*个按钮Button,而一个Button只能和一个Form类关联。此处不是双向关联,因为Button并不拥有Form对象的成员变量

    多重关联UML图示

  4. 聚合关系: 聚合(Aggregation)关系表示整体和部分的关系,成员对象是整体对象的一部分,但是成员对象可以脱离整体对象独立存在。成员对象的生命周期不由整体对象管理,通过构造方法、Setter方法注入到整体对象中。

    如下图,聚合关系用带空心菱形的实线表示

    聚合关系UML图示

  5. 组合关系: 组合(Composition)关系表示比聚合关系更强的整体与部分的关系,整体对象控制成员对象的生命周期。成员对象不能脱离整体对象而存在。通常在整体类的构造方法中直接实例化成员类。

    如下图,组合关系用带实心菱形的实线表示

    组合关系UML图示

依赖(Dependency)关系表示一个事物使用另一个事物时使用的依赖关系。一般情况下依赖关系体现在某个类的方法中使用另一个类的对象。由三种方式来实现:1、将一个类的对象作为另一个类中的方法参数。2、类的方法中使用另一个类的对象作为局部变量。3、在类的方法中调用另一个类的静态方法。

如下图,在UML中使用带箭头的虚线表示,由依赖的一方指向被依赖的一方。

组合关系UML图示

泛化(Generalization)关系就是集成关系,描述父类和子类之间的关系。

如下图,在UML中使用空心三角形的直线表示。

泛化关系UML图示

接口和类之间有实现(Realization)关系,类实现接口的所有生命方法。

如下图,在UML中使用空心三角形虚线的形式来表示实现关系。

接口与实现关系UML图示

面向对象设计目标之一在于可维护性复用,一方面要实现源代码的复用,一方面又要易于扩展和修改。以下七种面向对象设计原则就是为了支持可维护性复用而诞生。

设计模式原则
设计模式原则

单一职责原则(Single Responsibility Principle, SRP): 一个类只负责一个功能领域的相应职责。这样一个类就只有一个引起它变化的原因

一个类承担的职责越多,其被复用的可能性就越小,并且当一个职责变化时可能会影响到其他职责。因此最好将不同的变化原因封装在不同的类中。

单一职责原则是实现高内聚、低耦合的指导方针,是最简单也最难运用的原则。

开闭原则(Open-Closed Principle, OCP): 一个软件实体应该对扩展开放,对修改关闭。软件应在尽量不修改原有代码的情况下进行扩展

里氏替换原则(Liskov Substitution Principle, LSP): 引用基类的地方可以透明地替换成其子类对象

依赖倒转原则(Dependency Inversion Principle, DIP): 抽象不应该依赖于细节,细节应该依赖于抽象。要针对接口编程,而不是针对实现编程

在大多数情况下,以上三种设计原则会同时出现,开闭原则是目标,里氏替换原则是基础,依赖倒转原则是手段。依赖方依赖接口编程(依赖倒转原则),通过替换接口的实现(需要里氏替换原则作为基础),从而实现在基本不修改依赖方的代码替换具体的实现达到对扩展开放对修改关闭的开闭原则。

接口隔离原则(Interface Segregation Principle, ISP): 使用多个专门的接口,而不使用单一的总接口。客户端不应该依赖那些它不需要的接口

“接口”有两种含义: 一种是类对外提供的方法集合,类型要满足单一职责原则,其对外提供的方法(接口)也应该要隔离其他类型;一种是语言提供的抽象类或者接口特性,每个接口应该只包含某一种用户定制的方法即可,接口不应该承担太多职责否则继承该接口的类需要实现不需要的方法。

合成复用原则(Composition Reuse Principle, CARP): 尽量使用对象组合,而不是继承来达到复用的目的。对象通过关联关系(包括组合关系和聚合关系)来使用另一个类的方法来达到复用的目的,而不是使用通过继承另一个类来使用其方法。

继承关系相比于关联关系会将基类的实现细节过多暴露给子类,所以其耦合程度高于关联关系。一般而言,如果两个类是“Has a”的关系应该使用组合或聚合;如果是“Is a”的关系,应该使用继承。像Go语言从语言层面不支持继承只支持组合也能成为流行的语言,说明继承也不是必须的,滥用继承反而会增加维护的难度以及系统的复杂度

迪米特法则(Law of Demeter, LoD): 一个软件实体应当尽可能减少与其它实体发生相互作用,从而降低系统的耦合度,类与类之间保持松散的耦合关系。

迪米特法则有几种定义形式:

  1. 不要和陌生人说话,只与直接朋友通信: 对于一个对象,其朋友有如下几类。对象不要和除以下朋友的陌生人直接通信。

    • 当前对象本身(this)
    • 以参数形式传入当前对象方法中的对象
    • 当前对象的成员对象
    • 当前对象所创建的对象
  2. 减少对象间的交互: 如果两个对象之间不必直接通信,则不应当发生任何直接的相互作用;如果其中一个对象需要调用另一个对象的方法,可以引入第三者转发这个调用,从而降低对象之间的耦合度。

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决