面向对象设计原则

如何同时提高一个软件系统的可维护性和可复用性是面向对象设计需要解决的核心问题之一。

在面向对象设计中,可维护性的复用是以设计原则为基础的。每一个原则都蕴含一些面向对象设计的思想,可以从不同的角度提升一个软件结构的设计水平。

单一职责原则

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

单一职责原则告诉我们:一个类不能太“累”!在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,接口的设计一定要做到单一职责,方法尽量做到一个方法只做一件事。类的设计由于种种情况,很难去完全做到单一职责原则,尽量去做到。

里氏代换原则

  • 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1代换o2时,程序P的行为没有变化,那么类型S是类型T的子类型。
  • 所有引用基类的地方必须能透明地使用其子类对象。

在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

子类必须完全实现父类的方法

根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果子类不能完整地实现父类的方法,正常的业务逻辑将不能运行。

子类可以有自己的个性

里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。有的业务依赖很强,应该直接使用子类。

输入输出

  • 覆盖或实现父类的方法时输入的参数可以被放大
  • 覆写或实现父类的方法时输出结果可以被缩小

依赖倒置原则

  • 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体,具体应该依赖于抽象。
    • 每个逻辑的实现是由原子逻辑组成的。不可分割的原子逻辑就是低层模块。原子逻辑的再组装就是高层模块。
    • 抽象是指接口或者抽象类,不能被实例化。细节是指实现接口或继承抽象类的具体类,可以被实例化。

依赖倒置原则在 Java 语言中的表现

  1. 模块间的依赖通过抽象类或接口发生,实现类之间不发生直接的依赖关系。
  2. 接口或抽象类不能依赖于实现类。
  3. 实现类依赖于接口或抽象类

采用依赖倒置原则可以减少类之间的耦合性,提高系统的稳定性, 降低并行开发引发的风险,提高代码可读性和可维护性。

依赖的三种写法

对象的依赖关系有三种方式来传递。

构造函数传递依赖对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface IDriver {
public void drive();
}

public class Driver implements IDriver{
private ICar car;
/**
* 在类的构造函数中声明依赖对象。
* 这种方法叫作--构造函数注入
*/
public Driver (ICar car){
this.car = car;
}
public void drive(){
this.car.run();
}
}

Setter 方法传递依赖对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface IDriver {
public void dirve();
/**
* 在抽象中设置Setter 方法声明依赖关系。
* 这种方法叫做--Setter 依赖注入
*/
public void setCar(ICar car);
}

public class Driver implements IDriver{
private ICar car;

public void setCar(ICar car){
this.car = car;
}
public void drive(){
this.car.run();
}
}

接口注入

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface IDriver {
/**
* 在抽象接口的方法参数中声明依赖对象
* 这种方法叫作--接口注入
*/
public void drive(ICar car);
}

public class Driver implements IDriver{
public void drive(ICar car){
car.run();
}
}

依赖倒置原则的使用

  1. 每个类尽量有自己的接口或者抽象类,后两者都有。
  2. 变量的表面类型尽量是接口或者抽象类。
  3. 任何一个类都不应该从具体类派生。
  4. 尽量不要复写基类已实现方法。

接口隔离原则

什么是Java接口

在Java中,接口可以被分为两种:

  • 实例接口(Object Interface),实例接口指的就是Java中的class,通过class文件产生的Java对象遵从该class文件所定义的接口,简单的说,就是类的非私有方法和属性。
  • 类接口(Class Interface),interface定义的接口。

接口隔离原则的定义

  • 客户端不应该依赖它不需要的接口。
  • 类之间的依赖尽量建立在最小的接口上,使用多个专门的接口,而不使用单一的总接口。

总结一下就是,接口尽量细化,建立单一接口,接口方法尽量要少。接口是对外提供的契约,分散定义多个接口,可以提高系统灵活性和可维护性。

保持接口的纯洁性

  • 接口尽量小
    根据单一职责原则,尽量小得拆分接口
  • 接口要高内聚
    接口中尽量少的公布public方法。
  • 定制服务
    只提供访问者需要的方法。
  • 接口设计并非越小越好,有一个限度

迪米特法则(最少知识原则)

一个软件实体应当尽可能少地与其他实体发生相互作用。

在迪米特法则中,对于一个对象,其朋友包括以下几类:

  • 当前对象本身(this;
  • 以参数形式传入到当前对象方法中的对象;
  • 当前对象的成员对象;
  • 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
  • 当前对象所创建的对象。

迪米特法则–低耦合

  • 只和朋友交流
    只和存在耦合关系(组合、聚合、依赖等)的对象交流,一般来说,朋友类出现在成员变量、方法的输入输出参数和构造器上。
  • 朋友间需要距离
    设计时尽量减少public属性和方法,多使用private,friendly(默认访问权限),protected等访问权限,以及是否可以加上final关键字。
  • 是自己的就是自己的
    如果一个方法放在本类中,即使不增加类间关系,也不对本类产生副作用,就放在本类中。
  • 谨慎使用Serializable

合成复用原则

尽量使用对象组合,而不是继承来达到复用的目的。
在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则。

如何使用合成复用原则

一般而言,如果两个类之间是“Has-A”的关系应使用组合或聚合,如果是“Is-A”关系可使用继承。“Is-A"是严格的分类学意义上的定义,意思是一个类是另一个类的"一种”;而"Has-A"则不同,它表示某一个角色具有某一项责任。

开闭原则

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。

如何使用开闭原则

  • 抽象约束
    • 通过接口或抽象类约束扩展,不允许出现在接口或抽象类中不存在的public方法;
    • 参数类型、引用对象尽量使用接口或抽象类;
    • 抽象层尽量稳定。
  • 元数据控制程序模块行为
  • 使用框架,建立项目章程
  • 封装变化
    • 将相同变化封装到统一接口;
    • 不同变化封装到不同接口;