设计原则概述
注意:所有的设计模式都不能违背《如何看懂源码》里描述的6大代码质量的评价标准:可维护性, 可读性, 可扩展性, 简洁性, 可复用性, 可测试性
设计原则和思想其实比设计模式更加普适和重要,掌握了代码的设计原则和思想,我们甚至可以自己创造出来新的设计模式。
SOLID 原则 -SRP 单一职责原则 Single Responsibility Principle
- 一个类或者模块只负责完成一个职责(或者功能)。
- 一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块。
- 另一种理解是:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。
- 一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块。
- 不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的。
- 实际上,在真正的软件开发中,我们也没必要过于未雨绸缪,过度设计。
- 所以,我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构
- 实际上,在真正的软件开发中,我们也没必要过于未雨绸缪,过度设计。
- 技巧
- 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
- 一个凑合能用的标准是: 一个类的代码行数最好不能超过 200 行,函数个数及属性个数都最好不要超过 10 个。
- 一个凑合能用的标准是: 一个类的代码行数最好不能超过 200 行,函数个数及属性个数都最好不要超过 10 个。
- 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
- 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
- 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
- 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。
- 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
SOLID 原则 -OCP 开闭原则 Open Closed Principle [非常重要]
在 23 种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵从的设计原则就是开闭原则。
- 软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。
添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
- 开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
- 同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。
- 一旦接口被修改了,会出现两个问题:
- 我们对接口进行了修改,这就意味着调用这个接口的代码都要做相应的修改。
- 相应的单元测试都需要修改。
- 不可过度追求扩展性,要和可读性保持一种平衡。
SOLID 原则 -LSP 里氏替换原则 Liskov Substitution Principle
- 子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
- 实际上,里氏替换原则还有另外一个更加能落地、更有指导意义的描述,那就是“Design By Contract”,中文翻译就是“按照协议来设计”。
- 子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。
- 这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
- 实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系。
- 实际上,里氏替换原则还有另外一个更加能落地、更有指导意义的描述,那就是“Design By Contract”,中文翻译就是“按照协议来设计”。
- LSP和多态的区别:
- 多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。
- 里氏替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
- 多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。
- 违背LSP原则的例子:
- 子类违背父类声明要实现的功能。
- 子类违背父类对输入、输出、异常的约定。
- 子类违背父类注释中所罗列的任何特殊说明。
- 子类违背父类声明要实现的功能。
SOLID 原则 -ISP 接口隔离原则 Interface Segregation Principle
- 客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。
- 把“接口”理解为一组 API 接口集合。
- 把“接口”理解为单个 API 接口或函数。
- 把“接口”理解为 OOP 中的接口概念。
- 把“接口”理解为一组 API 接口集合。
SOLID 原则 -DIP 依赖倒置原则 Dependency Inversion Principle [实现代码可测试性的重要工具]
- 高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。
- 所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。
- 所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。
DRY 原则、KISS 原则、YAGNI 原则、LOD 法则
- 如何写出满足 KISS 原则的代码?
- 不要使用同事可能不懂的技术来实现代码。比如正则表达式,还有一些编程语言中过于高级的语法等。
- 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。
- 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。
- 如果在 code review 的时候,同事对你的代码有很多疑问,那就说明你的代码有可能不够“简单”,需要优化啦。
- 不要使用同事可能不懂的技术来实现代码。比如正则表达式,还有一些编程语言中过于高级的语法等。
- YAGNI 跟 KISS 说的是一回事吗?
- YAGNI 原则的英文全称是:You Ain’t Gonna Need It。直译就是:你不会需要它。
- 不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计。
- 比如,我们的系统暂时只用 Redis 存储配置信息,以后可能会用到 ZooKeeper。根据 YAGNI 原则,在未用到 ZooKeeper 之前,我们没必要提前编写这部分代码。
- YAGNI 原则跟 KISS 原则并非一回事儿。KISS 原则讲的是“如何做”的问题(尽量保持简单),而 YAGNI 原则说的是“要不要做”的问题(当前不需要的就不要做)。
- YAGNI 原则的英文全称是:You Ain’t Gonna Need It。直译就是:你不会需要它。
- LOD: Law of Demeter(the Least Knowledge Principle)
- 每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。
- “高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。
- 每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。
越是不稳定的系统,我们越是要在代码的扩展性、维护性上下功夫。相反,如果某个系统特别稳定,在开发完之后,基本上不需要做维护,那我们就没有必要为其扩展性,投入不必要的开发时间。换言之:“不稳定的系统多使用设计模式,稳定的系统就没有必要为设计模式浪费时间”