细剖软件设计中的六大原则

2021-06-08 18:00:00

 给自己挖一个新坑  希望自己能后面能慢慢来填平

开新坑的原因

1. 先瞎bb最近的烦恼

 有时本意是写一个脚本,但由于业务不可预知性和复杂性,写着写着就搞了好多类出来。脚本越来越复杂。
 且每一次需求的变更。无疑是对代码结构设计的考验。
 往后今日再来看这个脚本,由于可读性差,短时间内根本看不出写了些啥。
 个人觉得代码量超过200行,其实奔着写脚本的目的去写就行不通了。
 这个时候就应该考虑构建架构,所谓万丈高楼平地起,基础夯实了,才能hold顶层的各种需求压力。

2. 开坑目的

 了解并学习软件设计中的六大原则,并记录心得

正篇

五大原则?六大原则?七大原则?

  1. 单一职责原则 (Single Responsibility Principle)
  2. 开放封闭原则 (Open-Closed Principle)
  3. 里氏替换原则 (Liskov Substitution Principle)
  4. 接口隔离原则 (Interface Segregation Principle)
  5. 依赖倒置原则 (Dependence Inversion Principle)
  6. 迪米特法则(Law Of Demeter)
  7. 组合/聚合复用原则 (Composite/Aggregate Reuse Principle)

按五大原则划分:1、2、3、4、5。(S.O.L.I.D 是面向对象设计(OOD)的头五大基本原则的首字母缩写,由俗称「鲍勃大叔」的 Robert C. Martin 提出)
按六大原则划分:多了6
按七大原则划分:多了7

(一)、单一职责原则

定义

就一个类而言,应该仅有一个引起它变化的原因。应该只有一个职责。

问题由来

T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。也就是说职责P1和P2被耦合在了一起。

(二)、开放封闭原则

定义

对扩展是开放的,而对修改是封闭的。
对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。

(三)、里氏替换原则

定义

派生类(子类)对象可以在程式中代替其基类(超类)对象

换言之
任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,即基类随便怎么改动子类都不受此影响,那么基类才能真正被复用

问题由来

因为继承带来的侵入性,增加了耦合性,也降低了代码灵活性,父类修改代码,子类也会受到影响,此时就需要里氏替换原则。

如何才算遵循里氏替换原则?

  1. 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
  2. 子类中可以增加自己特有的方法。
  3. 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

(四)、接口隔离原则

定义

客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。

(五)、依赖倒置原则

定义

程序要依赖于抽象接口,不要依赖于具体实现。
简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

归结于两点:

  1. 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
  2. 抽象不应该依赖于具体,具体应该依赖于抽象。

例子

《python3 依赖倒置原则示例》

(六)、迪米特法则

定义

一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
翻译成白话文: 一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

总结

迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系——这在一定程度上增加了系统的复杂度。 有兴趣可以研究一下设计模式的门面模式(Facade)和中介模式(Mediator),都是迪米特法则应用的例子。

(七)、组合/聚合复用原则

定义

在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。

先从组合和继承说起

在面向对象的设计里,有两种基本的方法可以在不同的环境中复用已有的设计和实现,即通过组合或通过继承。

组合

由于组合可以将已有的对象纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做有下面的好处:

  • 新对象存取成分对象的唯一方法是通过成分对象的接口。
  • 这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。
  • 这种复用支持包装。
  • 这种复用所需要的依赖较少。
  • 每一个新的类可以将焦点集中到一个任务上。
  • 这种复用可以在运行时间动态进行,新对象可以动态的引用与成分对象类型相同的对象。
  • 组合复用的缺点就是用组合复用建造的系统会有较多的对象需要管理。

继承

组合几乎可以用到任何环境中去,但是继承只能用到一些环境中。

继承复用通过扩展一个已有对象的实现来得到新的功能,基类明显的捕获共同的属性和方法,而子类通过增加新的属性和方法来扩展超类的实现。

继承的优点:

  • 新的实现比较容易,因为基类的大部分功能都可以通过继承自动的进入子类。
  • 修改或扩展继承而来的实现较为容易。

继承的缺点:(但这些缺点似乎可以用里氏替换原则规避?)

  • 继承复用破坏了包装,因为继承超类的的实现细节暴露给子类。由于超类的内部细节常常对子类是透明的,因此这种复用是透明的复用,又称“白箱”复用。
  • 如果超类的实现发生改变,那么子类的实现也不得不发生改变。因此,当一个基类发生改变时,这种改变就会像水中投入石子引起的水波一样,将变化一圈又一圈的传导到一级又一级的子类,使设计师不得不相应地改变这些子类,以适应超类的变化。
  • 从超类继承而来的实现是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。

组合还是继承?

按照组合复用原则我们应该首选组合,然后才是继承,使用继承时应该严格的遵守里氏替换原则,必须满足“Is-A”的关系是才能使用继承,而组合却是一种“Has-A”的关系。

陆续更新ing…