面向对象设计的七大设计原则

一、开闭原则(The Open-Closed Principle,OCP)

软件实体(模块,类,方法等)应该对扩展开放,对修改关闭。

实现方法:面向接口编程。

  • 把不变的部分抽象成不变的接口,这些不变的接口可以应对未来的扩展。
  • 接口的最小功能设计原则。原有的接口可以应对未来的扩展;不足的部分可以通过定义新的接口来实现。
  • 模块之间的调用通过抽象接口进行,这样即使实现层发生变化,也无需修改调用方的代码。

二、里氏替换原则(Liskov Substitution Principle,LSP)

所有引用基类的地方必须能透明地使用其派生类的对象。

LSP对我们是否应该使用继承提供了判断的依据,不再是简单地根据两者之间是否有相同之处来说使用继承。子类可以扩展父类的功能,但不能改变父类原有的功能:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类重载父类方法时,输入参数要比父类的更加宽松。
  • 当子类的方法实现父类的方法时,返回值要比父类更加严格。

三、迪米特原则(Law of Demeter, LoD)

又叫最少知道原则,只与你直接的朋友们通信,不要跟“陌生人”说话。

LoD可以降低类之间的耦合。

  • 一个软件实体应当尽可能少地与其他实体发生相互作用。
  • 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

朋友圈(方法可以调用的对象):


使用LoD应该注意:

  • LoD容易产生大量中介类,会增加系统的复杂度。
  • 朋友间也是有距离的。尽量不要对外公布过多的public方法和非静态变量,多使用private、protected。
  • 是自己的就是自己的。若一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,就放置在本类中。

四、单一职责原则(Single Responsibility Principle, SRP)

永远不要让一个类存在多个改变的理由。换句话说,只能让一个类/接口/方法有且仅有一个职责。

多个职责的缺点:

  • 一方面,如果一个职责使用了外部类库,则使用另一个职责的用户也不得不包含这个未被使用的外部类库。
  • 另一方面,修改一个职责,另外一个职责的用户也将受到影响,不得不重新编译和配置。

SRP的优点:

  • 从职责方面为我们对类(接口)的抽象的颗粒度建立了判断基准。
  • 降低了类的复杂度,提高了类的可读性,提高系统的可维护性,降低变更引起的风险。

五、接口分隔原则(Interface Segregation Principle, ISP)

不能强迫用户去依赖他们不使用的接口。

使用多个专门的接口比使用单一的总接口总要好。用户不应该因为不使用的接口的改变而改变。

  • ISP从对接口的使用上为我们对接口抽象的颗粒度建立了判断基准。
  • 符合高内聚低耦合的设计思想,使类具有很好的可读性、可扩展性和可维护性。
  • 接口分隔要适度,避免产生大量的细小接口。

实现方法:

  • 一个类对一个类的依赖应该建立在最小的接口上。
  • 建立单一接口,不要建立庞大臃肿的接口。
  • 尽量细化接口,接口中的方法尽量少。

单一职责原则和接口分隔原则的区别:

  • SRP强调的是职责,方法可以很多,针对程序中实现的细节。
  • ISP强调的是约束接口,针对抽象、整体框架。

六、依赖倒置原则(Dependency Inversion Principle, DIP)

A. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
B. 抽象不应该依赖于细节,细节应该依赖于抽象。
C. 针对接口编程,不要针对实现编程。
  • 依赖:模块A使用/调用模块B,称为A依赖B。
  • 低层模块:实现了一些基本操作的类。
  • 高层模块:封装了某些复杂的逻辑,并且依赖于低层模块的类。

高层模块依赖低层模块设计的问题:


  • 当低层模块需要修改或替换时,高层模块将受到影响。
  • 高层模块很难可以复用。

用DIP来解决问题:


  • 抽象接口层:对低层模块的抽象。
  • 高层模块依赖接口层,低层模块继承或实现抽象接口。
  • 类和类之间通过抽象接口层来建立关系。

DIP优点:
可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险。

七、组合/聚合复用原则(Composite/Aggregate Reuse Principle, CARP)

尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的。

在面向对象设计中,有两种基本的办法可以实现复用:第一种是通过组合/聚合,第二种就是通过继承。

什么时候才应该使用继承?只有当以下的条件全部被满足时,才应当使用继承关系:

  1. 派生类是基类的一个特殊种类,而不是基类的一个角色。"Is-A"使用继承,"Has-A"使用聚合。
  2. 永远不会出现需要将派生类换成另外一个类的派生类的情况。如果不能肯定将来是否会变成另外一个派生类的话,就不要使用继承。
  3. 派生类具有扩展基类的责任,而不是具有置换掉(override)或注销掉(Nullify)基类的责任。如果一个派生类需要大量的置换掉基类的行为,那么这个类就不应该是这个基类的派生类。
  4. 只有在分类学角度上有意义时,才可以使用继承。

例:


通过组合/聚合复用的优缺点
优点:

  1. 新对象存取子对象的唯一方法是通过子对象的接口。
  2. 这种复用是黑箱复用,因为子对象的内部细节是新对象所看不见的。
  3. 这种复用更好地支持封装性。
  4. 这种复用实现上的相互依赖性比较小。
  5. 每一个新的类可以将焦点集中在一个任务上。
  6. 这种复用可以在运行时间内动态进行,新对象可以动态的引用与子对象类型相同的对象。
  7. 作为复用手段可以应用到几乎任何环境中去。

缺点:

  1. 系统中会有较多的对象需要管理。

通过继承来进行复用的优缺点
优点:

  1. 新的实现较为容易,因为基类的大部分功能可以通过继承的关系自动进入派生类。
  2. 修改和扩展继承而来的实现较为容易。

缺点:

  1. 继承复用破坏封装性,因为继承将基类的实现细节暴露给派生类。由于基类的内部细节常常是对于派生类透明的,所以这种复用是透明的复用,又称“白箱”复用。
  2. 如果基类发生改变,那么派生类的实现也不得不发生改变。
  3. 从基类继承而来的实现是静态的,不可能在运行时间内发生改变,没有足够的灵活性。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容