程序设计中的基本原则

在编写一个系统的时候,我们总是希望我们的系统在设计上具备较好的可维护性和可扩展性,当客户需求有变,或者需要增加新功能时,能够从容应对,而一些前人总结的设计原则可以让我们在遇到这样的情况时候,不至于被动,从而能够以尽可能小的工作量来实现客户的需求。

开闭原则

OCP: Open-Closed Principle

开闭原则是指一个软件或系统应该 对扩展开放,对修改关闭 ,即在不修改原有代码的基础上实现需要扩展的功能。

所有的系统需求都会随着时间的推移而发生或大或小的变化,当系统面临新的需求时,一个好的设计应该只是增加新的代码,而不改变原有的设计,系统需要提供规范的接口来支持这样的扩展。比如我们的电脑,当我们需要更换或者扩展内存时,只需要买一块插进去即可,而不需要去修改主板上的接口设计。

满足开闭原则的系统,具备如下 两个优点

  1. 通过扩展已有的系统,可以提供新的功能,以满足新的需求,使变化中的系统具备一定的适应性和灵活性。
  2. 已有的系统模块,特别是最重要的抽象层模块无需更改,从而使变化中的系统具备一定的稳定性和延续性。

如何让系统满足开闭原则?

  • 抽象是关键

在面向对象设计语言中,可以对系统的需求进行抽象设计,而具体的实现则交给实现层,这样当有新的需求时,我们只需要扩展一个新的实现类,并实现之前的抽象设计接口,上层调用方式不需要改变,从而可以对扩展开放。

  • 封装可变性

将系统中容易变化的地方封装起来,当这些地方改变时不会导致重新设计。要做到对变化的封装,主要考虑如下 2 点:

  1. 变化不应该在代码中分散,而应该被封装在一个对象里面。相同可变性的不同表象应该反应在继承结构的具体子类中,所以继承应该被看做是封装变化的方法,而不应该被看做从一般对象生成特殊对象的方法。
  2. 不同的可变性不应该混淆在一起。要区分和抽离可变的点,当某一个点需要变化时做针对性的处理,而混淆的变化往往会牵一发而动全身。

里氏替换原则

LSP: Liskov Substitution Principle

里氏替换原则描述如下:

如果对于每一个类型 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 类型定义的所有程序 P,在将 o1 替换成 o2 时,程序 P 的行为没有发生任何变化,那么 T2 就是 T1 的子类型。

里氏替换原则被广泛用在接口参数设计中,即一个系统如果使用了基类的话,则一定适用于子类,并且系统不会察觉基类对象与子类对象的区别。比如说定义了一个方法 method(Base base),则我们可以将 Base 子类的对象作为参数传递进去。

里氏替换原则是继承复用的基石,只有衍生类可以替换掉基类,系统功能不会受到影响时,基类才能真正被复用,而衍生类也才能够在基类的基础上扩展新的行为。

依赖倒置原则

DIP: Dependency Inversion Principle

依赖倒置原则要求 要依赖于抽象,不要依赖于具体。

GoF的书中强调要 针对接口编程,不要针对实现编程 ,这是依赖倒置原则的一种表述。所要表达的意思是说应当使用接口或抽象类进行变量、参数,以及方法返回类型进行声明,而不是具体的实现类。

一个系统都是通过两个或多个类彼此合作来实现业务逻辑,这使得每个对象都需要引用与其合作的对象,如果这个引用获取的过程需要有对象自己来实现,则将导致代码高度耦合并且难以满足开闭原则。

在具体编码时我们可以通过一些设计模式,比如工厂方法模式、模板方法模式等,来将对象之间的引用耦合抽离出来单独管理,封装其可变性。而很多编程框架也提供了对这一原则的强大支持,比如 java 编程框架 Spring,就是将依赖倒置原则作为其核心功能,Spring 的 IoC 容器把对象之间引用管理的控制权从业务手中抽取出来由自己管理,并完成对对象的注入。

接口隔离原则

ISP: Interface Segregation Principle

接口隔离原则主张接口功能最小化,即使用多个专门的接口比使用一个单一的总接口要好。在这样的设计下可以对外暴露尽可能少的 API,从而让一个衍生类只关注自己需要的功能。

组合 / 聚合复用原则

CARP: Composition/Aggregation Principle

组合 / 聚合复用原则表述为要 多用组合,少用继承

在面向对象程序设计中,可以通过两种途径实现已有的设计的复用:组合/聚合和继承。

组合 / 聚合复用(Has-A) 通过组合已有的对象(也叫成分对象)到新的对象中,使之成为新对象的一部分,新的对象可以调用成分对象的功能,这种复用方式具有如下优点:

  1. 新对象通过成分对象接口来存储成分对象。
  2. 这种复用是黑箱复用,成分对象内部细节对于新对象是透明的。
  3. 动态复用,新对象可以动态引用成分对象。

组合 / 聚合复用的缺点是需要管理较多的成分对象。

继承复用(Is-A) 通过继承多个成分对象来得到新对象,这种方式的优点如下:

  1. 通过继承获得新功能,实现上较为简单。
  2. 修改或扩展继承得到的实现较为容易。

继承复用的缺点也是显而易见的:

  1. 这种复用是白箱复用,继承破坏了包装,因为父类的实现细节暴露给了子类。
  2. 如果超类发生变化,子类也需要做适应性调整。
  3. 从超类继承而来的特性都是静态的,不可能在运行时动态改变。

迪米特法则

LoD: Law of Demeter

迪米特法则也叫最少知识法则,就是说 一个对象应该对另一个对象有最少的了解 。比如对象 A 持有对象 B 的引用,对象 B 持有对象 C 的引用,如果此时 A 希望使用 C 的某个功能,则应该尽量通过 B 对象达到目的,而不是直接引用 C 对象。

除了在使用上注意,在类的设计上也要做好封装。在系统设计中,一个好的模块的设计标志就是该模块在多大程度上将自己的内部数据和其它与实现有关的细节隐藏了起来,彻底的将对外提供的 API 与自己的实现分隔开来。这样一来模块间的通信通过 API 完成,而不需要考虑模块的具体内部实现。这个可以解耦各个模块和子系统,从而实现独立的开发、优化、使用、阅读,以及修改。

迪米特法则的主要用途是 控制信息过载 ,在系统设计时,主要考虑以下几点:

  1. 在类的划分上应当创建弱耦合的类,这样有利于复用。
  2. 在类的结构上应当降低成员变量的访问权限,包装自己的 private 属性。
  3. 在类的设计上如果可能尽量设计成不变类。
  4. 在类的引用上一个类对其它对象的引用应该降到最低。