不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。我们在考虑应用某一个设计原则是否合理的时候,也可以以此作为最终的考量标准。
学习设计原则,要多问个为什么。不能把设计原则当真理,而是要理解设计原则背后的思想。搞清楚这个,比单纯理解原则讲的是啥,更能让你灵活应用原则。
理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?
-
如何理解单一职责原则(SRP)?
- 一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
-
如何判断类的职责是否足够单一?
-
需要根据实际情况。不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则
-
类中的代码行数、函数或者属性过多
-
类依赖的其他类过多,或者依赖类的其他类过多
-
私有方法过多
-
比较难给类起一个合适的名字
-
类中大量的方法都是集中操作类中的某几个属性
-
-
类的职责是否设计得越单一越好?
- 单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
理论二:如何做到“对扩展开放、修改关闭”?扩展和修改各指什么?(代码扩展性问题)
- 目的:只要它没有破坏原有的代码的正常运行,没有破坏原有的单元测试,我们就可以说,这是一个合格的代码改动。
-
如何理解“对扩展开放、对修改关闭”?
- 概念:添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性
等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。-
开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
-
同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。
- 我们要认识到,添加一个新功能,不可能任何模块、类、方法的代码都不“修改”,这个是做不到的。类需要创建、组装、并且做一些初始化操作,才能构建成可运行的的程序,这部分代码的修改是在所难免的。🟢我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。
-
- 概念:添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性
-
如何做到“对扩展开放、修改关闭”?
-
培养意识:我们要时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。
- 还有,在识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。
-
最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。
-
-
如何在项目中灵活应用开闭原则?
- 最合理的做法是,对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,我们就可以事先做些扩展性设计。
但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。
- 最合理的做法是,对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,我们就可以事先做些扩展性设计。
理论三:里式替换(LSP)跟多态有何区别?哪些代码违背了 LSP?
-
如何理解“里式替换原则”?
- 遵守协议,保证一致性:里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
-
与多态的区别
- 多态是语法特性,是一种实现方法。里式替换是设计原则,是一种规范。其存在的意义是用来规范我们对方法的使用,即指导我们如何正确的使用多态。
-
哪些代码明显违背了 LSP?
-
子类违背父类声明要实现的功能
-
子类违背父类对输入、输出、异常的约定
-
子类违背父类注释中所罗列的任何特殊说明
- 以上便是三种典型的违背里式替换原则的情况。除此之外,判断子类的设计实现是否违背里式替换原则,还有一个小窍门,那就是拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全地遵守父类的约定,子类有可能违背了里式替换原则。
-
-
评论区不错的理解
- 我觉得可以从两个角度谈里式替换原则的意义。
首先,从接口或父类的角度出发,顶层的接口/父类要设计的足够通用,并且可扩展,不要为子类或实现类指定实现逻辑,尽量只定义接口规范以及必要的通用性逻辑,这样实现类就可以根据具体场景选择具体实现逻辑而不必担心破坏顶层的接口规范。
从子类或实现类角度出发,底层实现不应该轻易破坏顶层规定的接口规范或通用逻辑
- 我觉得可以从两个角度谈里式替换原则的意义。
理论四:接口隔离原则有哪三种应用?原则中的“接口”该如何理解?
-
如何理解“接口隔离原则”?
-
把“接口”理解为一组 API 接口集合
- 可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
-
把“接口”理解为单个 API 接口或函数
- 部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
-
把“接口”理解为 OOP 中的接口概念
- 那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
-
-
接口隔离原则与单一职责原则的区别
- 单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。