It's our wits that make us men.

设计模式六大原则

Posted on By 刘电波

introduction

《设计模式之禅》的学习

单一职责原则

定义:

SRP的原话解释是:There should never be more than one reason for a class to change.

对于接口, 我们在设计的时候一定要做到单一, 但是对于实现类就需要多方面考虑了。 生搬硬套单一职责原则会引起类的剧增, 给维护带来非常多的麻烦, 而且过分细分类的职责也会人为地增加系统的复杂性。 本来一个类可以实现的行为硬要拆成两个类, 然后再使用聚 合或组合的方式耦合在一起, 人为制造了系统的复杂性。 所以原则是死的, 人是活的, 这句话很有道理。

对于单一职责原则, 我的建议是接口一定要做到单一职责, 类的设计尽量做到只有一个原因引起变化。

开闭原则

定义:

Software entities like classes,modules and functions should be open for extension but closed for modifications.

(一个软件实体如类、 模块和函数应该对扩展开放, 对修改关闭。 )

如上图所示,OffNovelBook类继承了NovelBook, 并覆写了getPrice方法,不修改原有的代码。仅仅覆写了getPrice方法, 通过扩展完成了新增加的业务(部分书籍打折)。

注意 开闭原则对扩展开放, 对修改关闭, 并不意味着不做任何修改, 低层模块的变更, 必然要有高层模块进行耦合, 否则就是一个孤立无意义的代码片段。

里氏替换原则

定义:

  • 第一种定义, 也是最正宗的定义:

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的子类型。 )

  • 第二种定义:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的 对象。 )

第二个定义是最清晰明确的, 通俗点讲, 只要父类能出现的地方子类就可以出现, 而且 替换为子类也不会产生任何错误或异常, 使用者可能根本就不需要知道是父类还是子类。 但 是, 反过来就不行了, 有子类出现的地方, 父类未必就能适应。

关于继承

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

  • 代码共享, 减少创建类的工作量, 每个子类都拥有父类的方法和属性;

  • 提高代码的重用性;

  • 子类可以形似父类, 但又异于父类, “龙生龙, 凤生凤, 老鼠生来会打洞”是说子拥有 父的“种”, “世界上没有两片完全相同的叶子”是指明子与父的不同;

  • 提高代码的可扩展性, 实现父类的方法就可以“为所欲为”了, 君不见很多开源框架的 扩展接口都是通过继承父类来完成的;

  • 提高产品或项目的开放性。

自然界的所有事物都是优点和缺点并存的, 即使是鸡蛋, 有时候也能挑出骨头来, 继承 的缺点如下:

  • 继承是侵入性的。 只要继承, 就必须拥有父类的所有属性和方法;

  • 降低代码的灵活性。 子类必须拥有父类的属性和方法, 让子类自由的世界中多了些约 束;

  • 增强了耦合性。 当父类的常量、 变量和方法被修改时, 需要考虑子类的修改, 而且在 缺乏规范的环境下, 这种修改可能带来非常糟糕的结果——大段的代码需要重构。

1.子类必须完全实现父类的方法

如果子类不能完整地实现父类的方法, 或者父类的某些方法在子类中已经发 生“畸变”, 则建议断开父子继承关系, 采用依赖、 聚集、 组合等关系代替继承。

2.子类可以有自己的个性

3.覆盖或实现父类的方法时输入参数可以被放大(重载)

不然会导致:子类在没有覆写父类的方法的前提下, 子类方法被执行了, 这会引起业务 逻辑混乱, 因为在实际应用中父类一般都是抽象类, 子类是实现类, 你传递一个这样的实现 类就会“歪曲”了父类的意图, 引起一堆意想不到的业务逻辑混乱, 所以子类中方法的前置条 件必须与超类中被覆写的方法的前置条件相同或者更宽松。

4. 覆写或实现父类的方法时输出结果可以被缩小(覆写)

总结

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

接口隔离原则

在讲接口隔离原则之前, 先明确一下我们的主角——接口。 接口分为两种:

  • 实例接口(Object Interface) , 在Java中声明一个类, 然后用new关键字产生一个实 例, 它是对一个类型的事物的描述, 这是一种接口。 比如你定义Person这个类, 然后使用 Person zhangSan=new Person()产生了一个实例, 这个实例要遵从的标准就是Person这个 类, Person类就是zhangSan的接口。 疑惑? 看不懂? 不要紧, 那是因为让Java语言浸染的时间 太长了, 只要知道从这个角度来看, Java中的类也是一种接口。

  • 类接口(Class Interface) , Java中经常使用的interface关键字定义的接口。

主角已经定义清楚了, 那什么是隔离呢? 它有两种定义, 如下所示:

  • 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. (类间的依赖关系应该建立在最小的接口上。 )

我们可以把这两个定义概括为一句话: 建立单一接口, 不要建立臃肿庞大的接口。 再通 俗一点讲: 接口尽量细化, 同时接口中的方法尽量少。 看到这里大家有可能要疑惑了, 这与 单一职责原则不是相同的吗? 错, 接口隔离原则与单一职责的审视角度是不相同的, 单一职 责要求的是类和接口职责单一, 注重的是职责, 这是业务逻辑上的划分, 而接口隔离原则要 求接口的方法尽量少。 例如一个接口的职责可能包含10个方法, 这10个方法都放在一个接口 中, 并且提供给多个模块访问, 各个模块按照规定的权限来访问, 在系统外通过文档约 束“不使用的方法不要访问”, 按照单一职责原则是允许的, 按照接口隔离原则是不允许的, 因为它要求“尽量使用多个专门的接口”。 专门的接口指什么? 就是指提供给每个模块的都应 该是单一接口, 提供给几个模块就应该有几个接口, 而不是建立一个庞大的臃肿的接口, 容 纳所有的客户端访问。

保证接口的纯洁性

  • 接口要尽量小,但是有限度的小。

  • 接口要高内聚

    • 什么是高内聚? 高内聚就是提高接口、 类、 模块的处理能力, 减少对外的交互。
    • 具体到接口隔离原则就是, 要求在接口中尽量 少公布public方法, 接口是对外的承诺, 承诺越少对系统的开发越有利, 变更的风险也就越 少, 同时也有利于降低成本。
  • 定制服务
    • 我们在做系统设计时也需要考虑对系统之间或模块之间的接口采用定制服务。 采用 定制服务就必然有一个要求: 只提供访问者需要的方法。
  • 接口设计是有限度的

    • 接口的设计粒度越小, 系统越灵活, 这是不争的事实。 但是, 灵活的同时也带来了结构 的复杂化, 开发难度增加, 可维护性降低, 这不是一个项目或产品所期望看到的, 所以接口 设计一定要注意适度。

实践

接口隔离原则是对接口的定义, 同时也是对类的定义, 接口和类尽量使用原子接口或原 子类来组装。 但是, 这个原子该怎么划分是设计模式中的一大难题, 在实践中可以根据以下 几个规则来衡量:

  • 一个接口只服务于一个子模块或业务逻辑;
  • 通过业务逻辑压缩接口中的public方法, 接口时常去回顾, 尽量让接口达到“满身筋骨 肉”, 而不是“肥嘟嘟”的一大堆方法;
  • 已经被污染了的接口, 尽量去修改, 若变更的风险较大, 则采用适配器模式进行转化 处理;
  • 了解环境, 拒绝盲从。 每个项目或产品都有特定的环境因素, 别看到大师是这样做的 你就照抄。 千万别, 环境不同, 接口拆分的标准就不同。 深入了解业务逻辑, 最好的接口设 计就出自你的手中!

依赖倒置原则

定义:

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语言中的表现就是:

  • 模块间的依赖通过抽象发生, 实现类之间不发生直接的依赖关系, 其依赖关系是通过 接口或抽象类产生的;

  • 接口或抽象类不依赖于实现类;

  • 实现类依赖接口或抽象类。

更加精简的定义就是“面向接口编程”——OOD(Object-Oriented Design, 面向对象设 计) 的精髓之一。

迪米特法则

定义:

迪米特法则(Law of Demeter, LoD) 也称为最少知识原则(Least Knowledge Principle, LKP) , 虽然名字不同, 但描述的是同一个规则: 一个对象应该对其他对象有最 少的了解。

我的知识你知道得越少越好

  • 只和朋友交流,迪米特法则还有一个英文解释是: Only talk to your immediate friends(只与直接的朋友通 信。 ) 什么叫做直接的朋友呢? 每个对象都必然会与其他对象有耦合关系, 两个对象之间的 耦合就成为朋友关系, 这种关系的类型有很多, 例如组合、 聚合、 依赖等。 下面我们将举例 说明如何才能做到只与直接的朋友交流。

  • 朋友间也是有距离的 ,朋友间相互公开的方法尽量少,

    • 一个类公开的public属性或方法越多, 修改时涉及的面也就越大, 变更引起的风险扩散 也就越大。 因此, 为了保持朋友类间的距离, 在设计时需要反复衡量: 是否还可以再减少 public方法和属性, 是否可以修改为private、 package-private(包类型, 在类、 方法、 变量前 不加访问权限, 则默认为包类型) 、 protected等访问权限, 是否可以加上final关键字等。
  • 是自己的就是自己的,如果一个方法放在本类中, 既不增加类间关 系, 也对本类不产生负面影响, 那就放置在本类中。

  • 谨慎使用Serializable

最佳实践

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

  • 迪米特法则要求类间解耦, 但解耦是有限度的, 除非是计算机的最小单元——二进制的 0和1。 那才是完全解耦, 在实际的项目中, 需要适度地考虑这个原则, 别为了套用原则而做 项目。 原则只是供参考, 如果违背了这个原则, 项目也未必会失败, 这就需要大家在采用原 则时反复度量, 不遵循是不对的, 严格执行就是“过犹不及”。