Design Pattern

设计模式原则

OCP 是最基础的一个原则,其余原则是 OCP 的具体形态

OCP

OCP是最基础的一个原则,其余原则是OCP的具体形态

  • OCP 可以提高复用性
  • OCP 可以提高可维护性
  • OCP 是面向对象开发的要求
  • OCP 对测试影响很大

Open Close Principle.Software entities like classes,modules and functions should be open for extension扩展 but closed for modifications修改.
一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化

  • 变化
    • 逻辑变化:
      只变化一个逻辑,而不涉及其他模块。比如原有的一个算法是a*b+c,现在需要修改为a*b*c,可以通过修改原有类中的方法来完成,前提条件是所有依赖或关联类都按照相同的逻辑处理。
    • 子模块变化:
      一个模块变化,会对其他的模块产生影响,特别是低层次的模块变化必然引起高层模块的变化。因此在通过扩展完成变化时,高层次的模块修改是必然的。
    • 可见视图变化:
      可见视图是提供给客户使用的界面,如 JSP程序Swing界面等,该部分的变化一般会引起连锁反应(特别是在国内做项目,做欧美的外包项目一般不会影响太大)。如果仅仅是界面上按钮文字的重新排布倒是简单;最司空见惯的是业务耦合变化:一个展示数据的列表,按照原有的需求是6列,突然有一天要增加1列,而且这一列要跨N张表,处理M个逻辑才能展现出来,这样的变化是比较恐怖的,但还是可以通过扩展来完成变化,这就要看原有的设计是否灵活。

注意:
在设计时尽量适应一些变化,以提高项目的稳定性和灵活性,真正实现拥抱变化
开闭原则对扩展开放,对修改关闭。并不是不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。
一个项目的基本路径应该是这样的:项目开发、重构、测试、投产、运维,其中的重构可以对原有的设计和代码进行修改,运维尽量减少对原有代码的修改,保持历史代码的纯洁性,提高系统的稳定性。

  • 如何使用
    • 抽象约束: 抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放。
      • 通过接口抽象类约束扩展,对扩展进行边界限定,不允许扩展出现在接口抽象类中的public方法
      • 参数类型、引用对象尽量使用接口抽象类,而不是实现类
      • 抽象层尽量保持稳定,一旦确定即不允许修改
  • 使用metadata元数据。即配置参数,用来描述环境和数据的数据,可以从文件、数据库中获得控制模块行为。尽量使用metadata来控制程序的行为,减少重复开发
  • 制定项目章程。即约定优于配置
  • 封装变化:封装可能发生的变化,即是protected variations受保护的变化。找出预计有变化或不稳定的点,为这些变化点创建稳定的接口,一旦预测到或“第六感”发觉有变化,就可以进行封装,23种设计模式即是从各个不同的角度对变化进行封装。
    • 将相同的变化封装到一个接口或抽象类中;
    • 将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。

DIP

Dependence Inversion Principle依赖倒置原则. High level modules should not depend upon low level modules.Both should depend upon abstractions.高层模块不应该依赖低层模块,两者都应该依赖其抽象. Abstractions should not depend upon details.抽象不应该依赖细节. Details should depend upon abstractions.细节应该依赖抽象

JAVA中的表现

  • Object-Oriented Design OOD 面向对象设计
  • 模块间的依赖通过抽象发生
  • 实现类之间不发生直接的依赖关系
  • 其依赖关系是通过接口或抽象类产生的
  • 接口或抽象类不依赖于实现类
  • 实现类依赖接口或抽象类

好处

  • 减少类间的耦合性
  • 提高系统的稳定性固化的、健壮的才是稳定的
  • 降低并行开发引起的风险
  • 提高代码的可读性和可维护性

依赖三种写法

  • 构造函数传递依赖对象
  • Setter方法传递依赖对象
  • 接口注入接口声明依赖对象

注意
稳定性固化的、健壮的才是稳定的较高的设计,在周围环境频繁变化的时候,依然可以做到“我自岿然不动”
Java中,只要定义变量就必然要有类型,一个变量可以有两种类型:表面类型实际类型

  • 表面类型:是在定义的时候赋予的类型
  • 实际类型:是对象的类型(具体类型new xxx())

TDDTest Driven Development 测试驱动开发模式就是依赖倒置原则的最高级应用

倒置
依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面向实现编程,这也是正常人的思维方式,我要开奔驰车就依赖奔驰车,我要使用笔记本电脑就直接依赖笔记本电脑。而编写程序需要的是对现实世界事物进行抽象,抽象的结果就是有了抽象类和接口,然后根据系统设计的需要产生抽象间的依赖,代替人们传统思维中事物间的依赖,倒置就来自于此。

最佳实践

  • 依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合
  • 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备
  • 变量的表面类型尽量是接口或者是抽象类
  • 任何类都不应该从具体类派生
  • 尽量不要重写基类的方法
  • 结合LSP里氏替换原则使用
    • 接口负责定义public属性和方法,并且声明与其他对象的依赖关系
    • 抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化

SRP

Single Responsibility Principle单一职责原则.
There should never be more than one reason for a class to change.应该有且仅有一个原因引起类的变更

好处

  • 类的复杂性降低,实现什么职责都有清晰明确的定义
  • 可读性提高
  • 可维护性提高

名词

  • BO业务对象 Business Object
  • Biz业务逻辑 Business Logic
  • VO值对象 Value Object

注意:
单一职责原则提出了一个编写程序的标准,用职责变化原因来衡量接口或类设计得是否优良。但是职责变化原因都是不可度量的,因项目而异,因环境而异。接口一定要做到单一职责。类的设计尽量做到只有一个原因引起变化。

ISP

Interface Segregation Principle接口隔离原则.
Clients should not be forced to depend upon interfaces that they don’t use.客户端不应该依赖它不需要的接口.
The dependency of one class to another one should depend on the smallest possible interface.类间的依赖关系应该建立在最小的接口上.

注意:
接口尽量细化,同时接口中的方法尽量少。单一职责要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分;而接口隔离原则要求接口的方法尽量少。

规范约束

  • 接口要尽量小
    • 不出现Fat Interface臃肿的接口
    • 根据接口隔离原则拆分接口时,首先必须满足单一职责原则
  • 接口要高内聚
    • 高内聚就是提高接口、类、模块的处理能力,减少对外的交互
    • 不讲任何条件、立刻完成任务的行为就是高内聚的表现
    • 要求在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本
  • 定制服务
    • 单独为一个个体提供优良的服务
    • 做系统设计时需要考虑对系统之间或模块之间的接口采用定制服务
    • 只提供访问者需要的方法
  • 接口设计是有限度的
    • 接口的设计粒度越小,系统越灵活
    • 灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低,所以接口设计一定要注意适度

最佳实践

  • 接口隔离原则是对接口的定义,同时也是对类的定义;接口和类尽量使用原子接口或原子类来组装
  • 一个接口只服务于一个子模块或业务逻辑
    • 通过业务逻辑压缩接口中的public方法
    • 接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
    • 已经被污染了的接口,尽量去修改
    • 若变更的风险较大,则采用适配器模式进行转化处理;

LoD_LKP

Law of Demeter迪米特法则Least Knowledge Principle最少知识原则.

Only talk to your immediate直接的 friends.

  • 一个对象应该对其他对象有最少的了解
  • 一个类应该对自己需要耦合或调用的类知道得最少

低耦合含义

  • Only talk to your immediate直接的 friends. 只和朋友交流,两个对象之间的耦合就称为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等。类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象JDK API提供的类除外
    • 朋友类的定义:
      • 出现在成员变量、方法的输入输出参数中的类称为成员朋友类
      • 而出现在方法体内部的类不属于朋友类
  • 朋友间也是有距离的: 一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。因此,为了保持朋友类间的距离,在设计时需要反复衡量:
    • 是否还可以再减少public方法和属性
    • 是否可以修改为privatepackage-private包类型,在类、方法、变量前不加访问权限,则默认为包类型protected等访问权限
    • 是否可以加上final关键字等
  • 是自己的就是自己的:
    如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中
  • 谨慎使用Serializable:
    项目中使用RMI RemoteMethod Invocation 远程方法调用方式传递一个VO Value Object 值对象,这个对象就必须实现Serializable接口仅仅是一个标志性接口,不需要实现具体的方法,也就是把需要网络传输的对象进行序列化,否则就会出现NotSerializableException异常。客户端的VO修改了一个属性的访问权限,从private变更为public,访问权限扩大了,如果服务器上没有做出相应的变更,就会报序列化失败。

注意:
迪米特法则要求类尽量不要对外公布太多的public方法和非静态public变量,尽量内敛,多使用privatepackage-privateprotected等访问权限

最佳实践:
高内聚低耦合: 迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚低耦合

LSP

Liskov Substitution Principle里氏替换原则.
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted替代 for o2 then S is a subtype of T如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。

1
2
3
4
5
6
S o1=new S();
T o2=new T();
...
P p=new P();
//如果o1 替换 o2后,P行为没有发生变化,则S是T的子类型
p.method(o2);

Functions that use pointers or references to base classes must be able to use objects of derived派生的 classes without knowing it所有引用基类的地方,即使不知道其子类信息,也必须能使用其子类的对象。但是,反过来就不行了,有子类出现的地方,父类未必就能适应.

继承: 在面向对象的语言中,继承是必不可少的、非常优秀的语言机制

  • 优点
    • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性
    • 提高代码的重用性
    • 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同
    • 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,很多开源框架的扩展接口都是通过继承父类来完成的
    • 提高产品或项目的开放性
    • 采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑
  • 缺点
    • 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
    • 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。

扩展

  • Java使用extends关键字来实现继承,它采用了单一继承的规则
  • C++则采用了多重继承的规则,一个子类可以继承多个父类。
  • 从整体上来看,利大于弊,怎么才能让“利”的因素发挥最大的作用,同时减少“弊”带来的麻烦呢?解决方案是引入里氏替换原则

规范

  • 子类必须完全实现父类的方法
  • 子类可以有自己的个性
  • 覆盖或实现父类的方法时输入参数可以被放大
    • 方法中的输入参数称为前置条件。
    • 里氏替换原则要求制定一个契约,就是父类或接口。契约制定了,也就同时制定了前置条件和后置条件。
    • 前置条件就是你要让我执行,就必须满足我的条件;
    • 后置条件就是我执行完了需要反馈(返回值),标准是什么
  • 重写或实现父类的方法时输出结果可以被缩小
    • 父类的一个方法的返回值是一个类型T,子类的相同方法重载或重写的返回值为S,里氏替换原则就要求S必须小于等于T。分两种情况
      • 如果是重写,父类和子类的同名方法的输入参数是相同的,两个方法的范围值S小于等于T,这是重写的要求,这才是重中之重。
      • 如果是重载,则要求方法的输入参数类型或数量不相同,在里氏替换原则要求下,就是子类的输入参数宽于或等于父类的输入参数,也就是说你写的这个方法是不会被调用的,参考上面讲的前置条件

注意

  • 在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则
  • 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承
  • downcast向下转型是不安全的,从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现
  • Override重写,参数,返回值一致
  • Overload重载,只有方法名一致,参数返回值可以不一致

最佳实践

  • 在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和
  • 把子类当做父类使用,子类的“个性”被抹杀;
  • 把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离缺乏类替换的标准