设计原则导读

常用设计原则解读 | 区别 | 联系

Posted by Booogu on April 3, 2021
4477 字 13 分钟

SOLID五大原则

单一职责原则——SRP

如何理解SRP

Single Responsibility Principle:一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性

如何判断类的职责是否单一?

不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。

实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:

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

类的职责是否设计得越单一越好?

单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

开闭原则——OCP

如何理解OCP?

Open Close Principle:添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。关于定义,我们有两点要注意。第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。

如何做到“对扩展开放、修改关闭”?

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

里氏替换原则——LSP

如何理解LSP?

Liskov Substitution Principle:理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字,要基于“contract”来理解什么行为会违反LSP,也就是找出,所谓“contract”中约定了哪几个维度的规范?

  • 宏观角度:父类要实现的功能,要做什么事?这一根本原则不允许子类违背
  • 微观角度21:父类入参的范围,入参的类型,不允许子类比父类制定的协议更严格,但允许比协议更宽松,比如父类说只能接收正整数,而子类可以接收负整数并处理,这种是不违背LSP的,可以扩展协议,进化为超集,但不能缩小协议,退化成子集
  • 微观角度2:可以理解为,协议中的“附加条款”,比如注释中的约定、异常的约定,一旦协议对此种细节有所说明,那么子类必须遵守,不允许打破

LSP与多态的联系与区别?

  • 联系:两者都体现了继承这一语言特性
  • 区别:多态是思想,描述的是“可行性”的维度,告诉我们,面向对象可以这样玩;LSP是原则,描述的是“标准性”的维度,告诉我们,面向对象虽然可以那样玩,但你最好这样玩。 从这个角度上讲,LSP约定了当程序中使用多态时,应当遵守怎样规范。

接口隔离原则——ISP

如何理解ISP?

Interface Segregation Principle: 理解“接口隔离原则”的重点是理解其中的“接口”二字。这里有三种不同的理解。

如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。

如果把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。

如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

ISP与SRP的区别?

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

接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一

案例:分析java.util.concurrent并发包中AtomicInteger原子类的getAndIncrease()方法,是否违背ISP/SRP?

案例个人分析:接口隔离,强调的是调用方,是否只使用了接口中的部分功能?若是,则违反接口隔离,应当细粒度拆分接口,从这个例子看,调用方诉求与方法名完全一致,通过方法内部封装两个操作,实现原子性,达成了调用方的最终目的,不多不少。 单一职责,不强调是否为调用方,只要能某一角度观察出,一个模块/类/方法,负责了多于一件事情,就可判定其破坏了单一职责,基于此经典理论,不假以深层次思考的角度出发,从方法本身的命名(做两件事)就可断定,它一定是破坏了单一职责的,应该拆分为两个操作。 但是,判定职责是否单一,要懂得结合业务场景与需求,从需求来看,此方法就是要通过JDK提供的CAS乐观自旋锁(方法最终依赖硬件指令集原语,Compare And Swap)从“原语”这一词的含义看,其实也是同时、原子性地做了一件“完整”的事情,因此,考虑这一点,是可以判定它符合单一职责的。

以上分析过程,可以看出,单一职责的判定往往见仁见智:基于不同的角度,不同的立场,不同的业务理解,往往可以得到不同的判定结果,但不必纠结,判定过程中用到的思想才是精髓。

依赖倒置/反转原则——DIP

什么是控制反转?

控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。

个人感悟:控制反转,是把对于核心业务流程无关的处理流程(除了doTest为核心业务逻辑,其余都是流程),由程序猿手动编码实现,“反转”为由框架来实现,把流程的控制权交给了框架,这是大多数框架能够提升开发效率的根本原因:大大降低了非核心业务逻辑的开发量;同时,这也引入了框架黑盒问题,这也是为什么我们要通读、熟悉、掌握一些核心框架(比如Spring)的原因,保证我们对框架源码有很深层次的理解和掌握,把交给框架的“控制权”拿回来,做到了然于胸,才能有更多思路,更高效、优雅地去配合框架机制,开发出更优秀的业务代码。

什么是依赖注入?

依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用

什么是依赖注入框架?

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

什么是依赖倒置原则?

包含两层含义:(1)高层模块不依赖低层模块,它们共同依赖同一个抽象。(2)抽象不要依赖具体实现细节,具体实现细节依赖抽象。

依赖倒置的精髓:在于接口/抽象所有权的倒置,高层不依赖低层,其实需要把低层的能力/协议抽象成接口,并且接口所有权属于高层,这样就能真正做到对高层的复用,而不依赖低层的实现细节,如果只是高层依赖低层的抽象/接口,那不是真正的依赖倒置。 可以结合好莱坞原则“Dont call me, I will call you”来理解,应用程序不需要调用Tomcat/Spring这样的框架来实现什么交互,而是由Tomcat/Spring拥有协议,应用程序实现此协议(比如实现Servlet接口),来完成交互。

KISS原则和YAGNI原则

如何理解KISS原则?

KISS 原则是保持代码可读和可维护的重要手段。KISS 原则中的“简单”并不是以代码行数来考量的。代码行数越少并不代表代码越简单,我们还要考虑逻辑复杂度、实现难度、代码的可读性等。而且,本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。除此之外,同样的代码,在某个业务场景下满足 KISS 原则,换一个应用场景可能就不满足了

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

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

KISS原则和YAGNI原则一样吗?

YAGNI原则核心思想:You Ain’t Gonna Need It。直译:你不会需要它。这条原则也算是万金油了。当用在软件开发中的时候,它的意思是:不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计,结合另外一个原则The Rule of Three理解

因此,YAGNI 原则跟 KISS 原则并非一回事儿。KISS 原则讲的是“如何做”的问题(尽量保持简单),而 YAGNI 原则说的是“要不要做”的问题(当前不需要的就不要做)

Dont Repeat Yourself原则——DRY

如何理解DRY原则?

关键在于理解Repeat的内涵:

  • 实现逻辑重复
  • 功能语义重复
  • 代码执行重复
  • 注释、文档重复
  • 数据对象重复(有字段可自动算出来没必要重复记录) 以上种种以及其他各种“重复”的扩展含义

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

迪米特法则——LOD

何为“高内聚,低耦合”?

“高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。

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

如何理解LOD?

LOD又称为最少知识原则,包含两层含义:(1)不该有直接依赖关系的类之间,不要有依赖(2)有依赖关系的类之间,尽量只依赖必要的接口。

LOD/基于接口而非实现编程/SRP/ISP/的区别?

原则 适用对象 侧重点 思考角度
单一职责原则 模块,类,接口 高内聚 自身
基于接口而非实现编程 接口,抽象类 低耦合 调用者
接口隔离原则 接口,函数 低耦合 调用者
迪米特法则 模块,类 低耦合 类关系