Java笔记 ·

漫谈设计原则

1. SOLID 原则

单一职责(S)、开闭原则(O)、里氏替换原则(L)、接口隔离原子(I)、依赖反转原则(D)。

1.1 SRP 单一职责

单一职责英文是Single Responsibility Principle(缩写,SRP),描述为A class or module should have a single reponsibility[一个类或者模块只负责完成一个职责(或功能)]。

单一职责通过避免设计大而全的类,避免将相关功能耦合在一起,来提高类的内聚性。

类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、松耦合。

但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

出现下面这些情况就有可能说明这类的设计不满足单一职责原则:

  • 类中的代码行数、函数或者属性过多;
  • 类依赖的其他类过多或者依赖类的其他类过多;
  • 私有方法过多;
  • 比较难给类起一个合适的名字;
  • 类中大量的方法都是集中操作类中的某几个属性。

1.2 OPC 开闭原则

添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。

注意:

  • 开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
  • 同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。

1.2.1 如何做到?

  1. 要时刻具备扩展意识、抽象意识、封装意识。

在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。

  1. 很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。

最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)

1.3 LSP 里氏替换原则

子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

里式替换原则是用来指导继承关系中子类该如何设计的一个原则。

父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数的原有“约定”。这里的“约定”包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

1.3.1 里式替换原则跟多态的区别

虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。

  • 多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。
  • 里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。

1.4 ISP 接口隔离原则

客户端不应该强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。理解“接口隔离原则”的重点是理解其中的“接口”二字。这里有三种不同的理解。

  • 把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
  • 把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
  • 把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

1.4.1 单一职责原则与接口隔离原则的区别

单一职责原则针对的是模块、类、接口的设计。

接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考的角度也是不同的。

1.4.2 判断接口的职责是否单一的标准

通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

1.5 DIP 依赖反转原则

依赖反转原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。中文翻译有时候也叫依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。

High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.

高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。

1.5.1 高层模块和低层模块的划分

简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。 以 Tomcat 这个 Servlet 容器作为例:

Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。

  • 按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。
  • Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。
  • Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。

1.5.2 控制反转(IOC)

控制反转的英文翻译是 Inversion Of Control,缩写为 IOC。

控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。

  • 控制”指的是对程序执行流程的控制
  • 反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架

1.5.3 依赖注入(DI)

依赖注入与控制反转恰恰相反,它是一种具体的编码技巧。依赖注入的英文翻译是 Dependency Injection,缩写为 DI。

我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或“注入”)给类来使用。

1.5.4 依赖注入框架(DI Framework)

通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。

2. KISS、YAGNI 原则

2.1 KISS 原则

KISS 原则的中文描述是:尽量保持简单。

KISS 原则是保持代码可读和可维护的重要手段。

2.1.1 如何写出满足 KISS 原则的代码

  • 不要使用同事可能不懂的技术来实现代码;
  • 不要重复造轮子,善于使用已经有的工具类库;
  • 不要过度优化。

2.2 YAGNI 原则

YAGNI 原则的英文全称是:You Ain’t Gonna Need It。直译就是:你不会需要它。

不要去设计当前用不到的功能;不要去编写当前用不到的代码。核心思想就是:不要做过度设计

2.3 KISS 原则 与 YAGNI 原则

  • KISS 原则讲的是“如何做”的问题(尽量保持简单)
  • YAGNI 原则说的是“要不要做”的问题(当前不需要的就不要做)。

3. DRY 原则

DRY 原则(Don’t Repeat Yourself)中文描述是:不要重复自己,将它应用在编程中,可以理解为:不要写重复的代码。

3.1 代码重复的情况

  • 实现逻辑重复
  • 功能语义重复
  • 代码执行重复

实现逻辑重复,但功能语义不重复的代码,并不违反 DRY 原则。

实现逻辑不重复,但功能语义重复的代码,也算是违反 DRY 原则。而代码执行重复也算是违反 DRY 原则。

3.2 代码复用性(Code Reusability)

  • 代码复用表示一种行为:我们在开发新功能的时候,尽量复用已经存在的代码。
  • 代码的可复用性表示一段代码可被复用的特性或能力:我们在编写代码的时候,让代码尽量可复用。
  • DRY 原则是一条原则:不要写重复的代码。

区别:

  • 不重复”并不代表“可复用”。在一个项目代码中,可能不存在任何重复的代码,但也并不表示里面有可复用的代码,不重复和可复用完全是两个概念。
  • “复用”和“可复用性”关注角度不同。代码“可复用性”是从代码开发者的角度来讲的,“复用”是从代码使用者的角度来讲的。

3.3 提高代码复用性

  • 减少代码耦合
  • 满足单一职责原则
  • 模块化
  • 业务与非业务逻辑分离
  • 通用代码下沉
  • 继承、多态、抽象、封装
  • 应用模板等设计模式

在设计每个模块、类、函数的时候,要像设计一个外部 API 一样去思考它的复用性。相比于代码的可复用性,DRY 原则适用性更强些。我们可以不写可复用的代码,但一定不能写重复的代码。

4. LOD 原则

迪米特法则的英文翻译是:Law of Demeter,缩写是 LOD,也叫最小知识原则(The Least Knowledge Principle)。

Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.

每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。

概念:不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口

迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。

4.1 高内聚,松耦合

“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。

高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围

  • 高内聚是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中
  • 松耦合指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动

参考资料

参与评论