面向对象的设计原则(三)

5.接口隔离原则(Interface Segregation Principle, ISP)


  接口隔离原则与单一职责原则在思想上比较相似,都是为了降低类之间的耦合度。接口隔离原则的定义如下:

  使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。

  根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干,使接口的职责明确。在面向对象编程语言中,实现一个接口就需要实现该接口中定义的所有方法。因此如果接口方法过多,其实现类中就不得不去处理许多自己根本不需要的方法。故大的总接口使用起来不一定很方便,为了使接口的职责单一,需要将大接口中的方法根据其职责不同分别放在不同的小接口中,以确保每个接口使用起来都较为方便,并都承担某一单一角色。

  下面通过一个简单实例来加深对接口隔离原则的理解:

  开发人员针对某CRM系统的客户数据显示模块设计了如图所示接口,其中方法dataRead()用于从文件中读取数据,方法transformToXML()用于将数据转换成XML格式,方法createChart()用于创建图表,方法displayChart()用于显示图表,方法createReport()用于创建文字报表,方法displayReport()用于显示文字报表。

 

  在实际使用过程中发现该接口很不灵活,例如如果一个具体的数据显示类无须进行数据转换(源文件本身就是XML格式),但由于实现了该接口,将不得不实现其中声明的transformToXML()方法(至少需要提供一个空实现);如果需要创建和显示图表,除了需实现与图表相关的方法外,还需要实现创建和显示文字报表的方法,否则程序编译时将报错。

  由于在接口CustomerDataDisplay中定义了太多方法,即该接口承担了太多职责,一方面导致该接口的实现类很庞大,在不同的实现类中都不得不实现接口中定义的所有方法,灵活性较差,如果出现大量的空方法,将导致系统中产生大量的无用代码,影响代码质量;另一方面由于客户端针对大接口编程,将在一定程序上破坏程序的封装性,客户端看到了不应该看到的方法,没有为客户端定制接口。因此需要将该接口按照接口隔离原则和单一职责原则进行重构,将其中的一些方法封装在不同的小接口中,确保每一个接口使用起来都较为方便,并都承担某一单一角色,每个接口中只包含一个客户端(如模块或类)所需的方法即可。通过使用接口隔离原则,本实例重构后的结构如图所示。

 

  在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不用的方法。


6.迪米特法则(Law of Demeter, LoD)

  

  迪米特法则来自于1987年美国东北大学(Northeastern University)一个名为“Demeter”的研究项目。迪米特法则又称为最少知识原则(LeastKnowledge Principle, LKP),其定义如下

  一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。

  如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。

  迪米特法则还有几种定义形式,包括:不要和“陌生人”说话、只与你的直接朋友通信等,在迪米特法则中,对于一个对象,其朋友包括以下几类:

  (1) 当前对象本身(this);

  (2) 以参数形式传入到当前对象方法中的对象;

  (3) 当前对象的成员对象;

  (4) 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;

  (5) 当前对象所创建的对象。

  任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。在应用迪米特法则时,一个对象只能与直接朋友发生交互,不要与“陌生人”发生直接交互,这样做可以降低系统的耦合度,一个对象的改变不会给太多其他对象带来影响。

  迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。

在将迪米特法则运用到系统设计中时,要注意下面的几点:

  • 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;
  • 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;在类的设计上,只要有可能,一个类型应当设计成不变类;
  • 在对其他类的引用上,一个对象对其他对象的引用应当降到最低。

  公司所开发CRM系统包含很多业务操作窗口,在这些窗口中,某些界面控件之间存在复杂的交互关系,一个控件事件的触发将导致多个其他界面控件产生响应,例如,当一个按钮(Button)被单击时,对应的列表框(List)、组合框(ComboBox)、文本框(TextBox)、文本标签(Label)等都将发生改变,在初始设计方案中,界面控件之间的交互关系可简化为如图所示结构:

 

  在图中,由于界面控件之间的交互关系复杂,导致在该窗口中增加新的界面控件时需要修改与之交互的其他控件的源代码,系统扩展性较差,也不便于增加和删除新控件。现使用迪米特对其进行重构。

  在本实例中,可以通过引入一个专门用于控制界面控件交互的中间类(Mediator)来降低界面控件之间的耦合度。引入中间类之后,界面控件之间不再发生直接引用,而是将请求先转发给中间类,再由中间类来完成对其他控件的调用。当需要增加或删除新的控件时,只需修改中间类即可,无须修改新增控件或已有控件的源代码,重构后结构如图所示。



 

7.合成复用原则(Composite Reuse Principle, CRP)


  合成复用原则又称为组合/聚合复用原则(Composition/Aggregate Reuse Principle, CARP),其定义如下:

  尽量使用对象组合,而不是继承来达到复用的目的。

  合成复用原则就是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用功能的目的。简言之:复用时要尽量使用组合/聚合关系(关联关系),少用继承。

  在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。

  通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的内部细节通常对子类来说是可见的,所以这种复用又称“白箱”复用,如果基类发生改变,那么子类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;而且继承只能在有限的环境中使用(如类没有声明为不能被继承)。

  由于组合或聚合关系可以将已有的对象(也可称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用,相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。

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

  下面通过一个简单实例来加深对合成复用原则的理解:

  公司开发人员在初期的CRM系统设计中,考虑到客户数量不多,系统采用MySQL作为数据库,与数据库操作有关的类如CustomerDAO类等都需要连接数据库,连接数据库的方法getConnection()封装在DBUtil类中,由于需要重用DBUtil类的getConnection()方法,设计人员将CustomerDAO作为DBUtil类的子类,初始设计方案结构如图所示。

 

  随着客户数量的增加,系统决定升级为Oracle数据库,因此需要增加一个新的OracleDBUtil类来连接Oracle数据库,由于在初始设计方案中CustomerDAO和DBUtil之间是继承关系,因此在更换数据库连接方式时需要修改CustomerDAO类的源代码,将CustomerDAO作为OracleDBUtil的子类,这将违反开闭原则。

  根据合成复用原则,我们在实现复用时应该多用关联,少用继承。因此在本实例中我们可以使用关联复用来取代继承复用,重构后的结构如图所示:

 

  在图中,CustomerDAO和DBUtil之间的关系由继承关系变为关联关系,采用依赖注入的方式将DBUtil对象注入到CustomerDAO中,可以使用构造注入,也可以使用Setter注入。如果需要对DBUtil的功能进行扩展,可以通过其子类来实现,如通过子类OracleDBUtil来连接Oracle数据库。由于CustomerDAO针对DBUtil编程,根据里氏代换原则,DBUtil子类的对象可以覆盖DBUtil对象,只需在CustomerDAO中注入子类对象即可使用子类所扩展的方法。例如在CustomerDAO中注入OracleDBUtil对象,即可实现Oracle数据库连接,原有代码无须进行修改,而且还可以很灵活地增加新的数据库连接方式。


参考文献:
1、大话设计模式

2http://blog.csdn.net/lovelion/article/details/17517213

【作者:墨叶扶风 http://blog.csdn.net/yefufeng


猜你喜欢

转载自blog.csdn.net/yefufeng/article/details/78668852