Java设计模式六大原则

34次阅读

共计 6554 个字符,预计需要花费 17 分钟才能阅读完成。

1、单一职能原则(Single Responsibility Principle, SRP)

定义

There should never be more than one reason for a class to change.

应该有且仅有一个原因引起类的变更

换言之,也就是一个接口或类只有一个职责

好处

  • 类的复杂性降低,实现什么职责都有清晰明确的定义;
  • 可读性提高,复杂性降低,那当然可读性提高了;
  • 可维护性提高,可读性提高,那当然更容易维护了;
  • 变更引起的风险降低,变更时必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

最佳实践

职责的划分很难,要想完全符合单一职责的设计更难,原则是接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化

2、里氏替换原则(LiskovSubstitution Principle,LSP)

继承的利与弊

  • 优点

    • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
    • 提高代码的重用性;
    • 子类可以形似父类,但又异于父类;
    • 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了;
    • 提高产品或项目的开放性。
  • 缺点

    • 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
    • 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
    • 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。

定义

If for each object o1 of type S there is an object o2 oftype T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 issubstituted for o2 then S is a subtype of T.

如果对每一个类型为 S 的对象 o1,都有类型为 T 的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 S 是类型 T 的子类型。

以上是 LSP” 最正宗 ” 的定义,但是可能不太容易理解,它还有另外一种相对清晰明确的定义:

Functions that use pointers or references to base classes must be able to useobjects of derived classes without knowing it.

所有引用基类的地方必须能透明地使用其子类的对象。

通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。(LSP 可以正着用但不能反着用)

规范

里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了 4 层含义:

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

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

  • 子类可以有自己的个性
  • 覆盖或实现父类的方法时输入参数可以被放大(重载而非覆盖)
  • 覆写或实现父类的方法时输出结果可以被缩小

    父类的一个方法的返回值是一个类型 T,子类的相同方法(重载或覆写)的返回值为 S,那么里氏替换原则就要求 S 必须小于等于 T,也就是说,要么 S 和 T 是同一个类型,要么 S 是 T 的子类。

最佳实践

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

3、依赖倒置原则(Dependence Inversion Principle,DIP)

定义

High level modules should not depend upon low level modules.Both should depend uponabstractions.Abstractions should not depend upon details.Details should depend upon abstractions.

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
  • 抽象不应该依赖细节;
  • 细节应该依赖抽象。

依赖倒置原则在 Java 中的体现:

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

更加精简的定义就是“面向接口编程”。

依赖的三种写法

  • 构造函数传递依赖对象(构造函数注入),在类中通过构造函数声明依赖对象
  • Setter 方法传递依赖对象(Setter 依赖注入),在抽象中设置 Setter 方法声明依赖关系
  • 在接口的方法中声明依赖对象(接口注入)

最佳实践

依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合,我们怎么在项目中使用这个规则呢?只要遵循以下的几个规则就可以:

  • 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备

    这是依赖倒置的基本要求,接口和抽象类都是属于抽象的,有了抽象才可能依赖倒置

  • 变量的表面类型尽量是接口或者是抽象类
  • 任何类都不应该从具体类派生
  • 尽量不要覆写基类的方法
  • 结合里氏替换原则使用

    父类出现的地方子类就能出现,再 DIP,我们可以得出这样一个通俗的规则:接口负责定义 public 属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

依赖倒置原则是 6 个设计原则中最难以实现的原则,它是实现开闭原则的重要途径,依赖倒置原则没有实现,就别想实现对扩展开放,对修改关闭。在项目中,大家只要记住是“面向接口编程”就基本上抓住了依赖倒置原则的核心。

4、接口隔离原则(Interface Segregations Principle, ISP)

定义

接口隔离原则有以下两种定义:

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.

类间的依赖关系应该建立在最小的接口上。

解释:要求是最小的接口,也是要求接口细化,接口纯洁,与第一个定义如出一辙,只是一个事物的两种不同描述。

概括:建立单一接口,不要建立臃肿庞大的接口。再通俗一点讲:接口尽量细化,同时接口中的方法尽量少。

接口隔离原则 VS 单一职责原则

  • 接口隔离原则与单一职责的审视角度是不相同的
  • 单一职责要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分;
  • 接口隔离原则要求接口的方法尽量少。

例如一个接口的职责可能包含 10 个方法,这 10 个方法都放在一个接口中,并且提供给多个模块访问,各个模块按照规定的权限来访问,在系统外通过文档约束“不使用的方法不要访问”,按照单一职责原则是允许的,按照接口隔离原则是不允许的,因为它要求“尽量使用多个专门的接口”。专门的接口指什么?就是指提供给每个模块的都应该是单一接口,提供给几个模块就应该有几个接口,而不是建立一个庞大的臃肿的接口,容纳所有的客户端访问。

// TODO 还是感觉差不太多 …

规范

  • 接口要尽量小

    • 要有限度
    • 不能违背单一职责原则(业务上的划分)
  • 接口要高内聚

    高内聚具体到接口隔离原则就是,要求在接口中尽量少公布 public 方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本。

  • 定制服务(只提供访问者需要的方法)

    举例说明:一个用于查询用户的接口,应该提供具体的根据用户 ID 查询或者根据用户名查询等于业务相关的特定接口,而不是提供一个带有 Map(或者说类似 Mybatis Example)参数的查询接口

  • 接口设计是有限度的

    • 接口的设计粒度越小,系统越灵活;
    • 但是,灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低,这不是一个项目或产品所期望看到的;
    • 所以接口设计一定要注意适度,这个“度”如何来判断呢?根据经验和常识判断,没有一个固化或可测量的标准。

最佳实践

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

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

5、迪米特法则(Law of Demeter,LoD)

也称为最少知识原则(Least KnowledgePrinciple,LKP)

定义

一个对象应该对其他对象有最少的了解

通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的这么多 public 方法,我就调用这么多,其他的我一概不关心。

含义

  • 只和朋友交流

    迪米特法则还有一个英文解释是:

    Only talk to your immediate friends.

    只与直接的朋友通信。

    一个类只和朋友交流,不与陌生类交流,不要出现 getA().getB().getC().getD()这种情况(在一种极端的情况下允许出现这种访问,即每一个点号后面的返回类型都相同),类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象,当然,JDK API 提供的类除外。

  • 朋友间也是有距离的

    迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的 public 方法和非静态的 public 变量,尽量内敛,多使用 private、package-private、protected 等访问权限。

  • 是自己的就是自己的

    在实际应用中经常会出现这样一个方法:放在本类中也可以,放在其他类中也没有错,那怎么去衡量呢?你可以坚持这样一个原则:如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。

  • 谨慎使用 Serializable

最佳实践

  • 迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚低耦合。
  • 迪米特法则要求类间解耦,但解耦是有限度的,除非是计算机的最小单元——二进制的 0 和 1。那才是完全解耦,在实际的项目中,需要适度地考虑这个原则,别为了套用原则而做项目。原则只是供参考,如果违背了这个原则,项目也未必会失败,这就需要大家在采用原则时反复度量,不遵循是不对的,严格执行就是“过犹不及”。

6、开闭原则(Open Closed Principle, OCP)

开闭原则是 Java 世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统

定义

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

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

换句话说,一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。

重要性

开闭原则是最基础的一个原则,前面节介绍的五个原则都是开闭原则的具体形态,也就是说前五个原则就是指导设计的工具和方法,而开闭原则才是其精神领袖。换一个角度来理解,依照 Java 语言的称谓,开闭原则是抽象类,其他五大原则是具体的实现类,开闭原则在面向对象设计领域中的地位就类似于牛顿第一定律在力学、勾股定律在几何学、质能方程在狭义相对论中的地位,其地位无人能及。

如何使用开闭原则

  • 抽象约束

    • 通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的 public 方法;
    • 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;
    • 抽象层尽量保持稳定,一旦确定即不允许修改。
  • 元数据(metadata)控制模块行为

    何谓元数据?就是用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。

  • 制定项目章程(团队约定)
  • 封装变化

    • 将相同的变化封装到一个接口或抽象类中;
    • 将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。
    • 封装变化,也就是受保护的变化(protected variations),找出预计有变化或不稳定的点,我们为这些变化点创建稳定的接口,准确地讲是封装可能发生的变化,一旦预测到或“第六感”发觉有变化,就可以进行封装,23 个设计模式都是从各个不同的角度对变化进行封装的

最佳实践

  • 开闭原则也只是一个原则

    开闭原则只是精神口号,实现拥抱变化的方法非常多,并不局限于这 6 大设计原则,但是遵循这 6 大设计原则基本上可以应对大多数变化。因此,我们在项目中应尽量采用这 6 大原则,适当时候可以进行扩充,例如通过类文件替换的方式完全可以解决系统中的一些缺陷。大家在开发中比较常用的修复缺陷的方法就是类替换,比如一个软件产品已经在运行中,发现了一个缺陷,需要修正怎么办?如果有自动更新功能,则可以下载一个.class 文件直接覆盖原有的 class,重新启动应用(也不一定非要重新启动)就可以解决问题,也就是通过类文件的替换方式修正了一个缺陷,当然这种方式也可以应用到项目中,正在运行中的项目发现需要增加一个新功能,通过修改原有实现类的方式就可以解决这个问题,前提条件是:类必须做到高内聚、低耦合,否则类文件的替换会引起不可预料的故障。

  • 项目规章非常重要

    如果你是一位项目经理或架构师,应尽量让自己的项目成员稳定,稳定后才能建立高效的团队文化,章程是一个团队所有成员共同的知识结晶,也是所有成员必须遵守的约定。优秀的章程能带给项目带来非常多的好处,如提高开发效率、降低缺陷率、提高团队士气、提高技术成员水平,等等。

  • 预知变化

    在实践中过程中,架构师或项目经理一旦发现有发生变化的可能,或者变化曾经发生过,则需要考虑现有的架构是否可以轻松地实现这一变化。架构师设计一套系统不仅要符合现有的需求,还要适应可能发生的变化,这才是一个优良的架构。

开闭原则是一个终极目标,任何人包括大师级人物都无法百分之百做到,但朝这个方向努力,可以非常显著地改善一个系统的架构,真正做到“拥抱变化”。

7、总结

首先,无论是 6 大设计原则还是 23 种设计模式,根本目标其实就是一个 ”拥抱变化“,包括需求的变化、运行环境的变化、性能要求的提升等等,实现一个需求并不难,但是当变化来临时,能否泰然处之,那就是个技术活了。

其次,” 拥抱变化 ” 落实到代码层面是什么?——你的代码是 可维护的、可扩展的,还是说 ” 牵一发动全身 ”,一点小的改动很多东西都要跟着变动。

再次,要做到 ” 拥抱变化 ”,让你的代码很容易维护和扩展,核心理念就是 “ 高内聚、低耦合 ”,以及 ” 面向接口编程(面向抽象编程)

最后,设计原则、设计模式是前辈总结的 ” 经验 ”,但不是 ” 条款 ”,尽可能遵循这些规范会让你的设计无限接近完美,但世界上本就没有十全十美的东西,凡事都要有个度,不要认死理,不要为了 ” 套模式 ” 而应用设计模式,要具体问题具体分析,根据实际情况进行权衡。

设计做得再漂亮,代码写得再完美,项目做得再符合标准,一旦项目亏本,产品投入大于产出,那整体就是扯淡!

参考文献:《设计模式之禅》

正文完
 0