客户端不应该依赖那些它不需要的接口。
实验一
考虑一个安全系统。在这个系统中,有一些Door对象,可以被加锁和解锁,并且Door对象知道自己是开着还是关着。这个Door编码成一个接口,这样客户程序就可以使用那些符合Door接口的对象,而不需要依赖于Door的特定实现。
现在,考虑一个这样的实现,TimedDoor,如果门开着的时间过长,它就会发出警报声。为了做到这一点,TimedDoor对象需要和另一个名为Timer的对象交互。
如果一个对象希望得到超时通知,它可以调用Timer的Register函数。该函数有两个参数,一个是超时时间,另一个是指向TimerClient对象的引用,其TimeOut函数会在超时到达时被调用。
怎样将TimerClient类和TimedDoor类联系起来,才能在超时时通知到TimedDoor中相应的处理代码呢?比如下面的一种实现:
这种做法的问题是,现在Door类依赖于TimerClient了。可是并不是所有种类的Door都需要定时功能。事实上,最初的Door抽象类和定时功能没有任何关系。如果创建了无需定时功能的Door的派生类,那么在这些派生类中就必须要提供TimeOut方法的退化实现,这就有可能违反LSP。此外,使用这些派生类的应用程序即使不使用TimerClient类的定义,也必须要引入它。
这是一个接口污染的例子,Door的接口被一个它不需要的方法污染了。在Door的接口中加入这个方法只是为了能给它的一个子类带来好处。如果持续这样做的话,那么每次子类需要一个新方法时,这个方法就会加到基类中去。这会进一步污染基类的接口,使它变"胖"。
此外,每次基类中加入一个方法时,派生类中就必须要实现这个方法(或者定义一个默认实现)。事实上,有一种特定的相关实践,可以使派生类无需实现这些方法,该实践的做法是把这些接口合并为一个基类,并在这个基类中提供接口中方法的退化实现。但是我们前面已经学过,这种实践违反了LSP,会带来维护和重用方面的问题。
请根据接口隔离原则,重构上面的设计。
解析(参考):
一个解决方案是**创建一个派生自TimerClient的对象,并把对该对象的请求委托给TimedDoor。**当TimedDoor想要向Timer对象注册一个超时请求时,它就创建一个DoorTimerAdapter并且把它注册给Timer。当Timer对象发送TimeOut消息给DoorTimerAdapter时,DoorTimerAdapter把这个消息委托给TimedDoor。这个解决方案遵循ISP原则,并且避免了Door的客户程序和Timer之间的耦合。即使对代码清单12-3中所示的Timer进行了改变,也不会影响到任何Door的使用者。此外,TimedDoor也不必具有和TimerClient一样的接口。DoorTimerAdapter会将TimerClient接口转换成TimedDoor接口。因此,这是一个非常通用的解决方案。
实验二
某软件公司开发人员针对 CRM 系统的客户数据显示模块设计了如下图所示的 CustomerDataDisplay 接口。其中:
方法 readData() 用于从文件中读取数据;
方法 transformToXML() 用于将数据转换成 XML 格式;
方法 createChart() 用于创建图表;
方法 displayChart() 用于显示图表;
方法 createReport() 用于创建文字报表;
方法 displayReport() 用于显示文字报表。
在实际使用过程中发现该接口很不灵活。例如:如果一个具体的数据显示类无须进行数据转换(源文件本身就是 XML 格式),但由于实现了该接口,将不得不实现其中声明的 transformToXML() 方法(至少需要提供一个空实现);如果需要创建和显示图表,除了需要实现与图表相关的方法外;还需要实现创建和显示文字报表的方法。否则程序在编译时将报错。
现使用接口隔离原则对其进行重构。
解析(参考):
在本实例中,由于在接口 CustomerDataDisplay 中定义了太多方法,即该接口承担了太多职责,一方面导致该接口的实现类很庞大,在不同的实现类中都不得不实现接口中定义的所有方法,灵活性较差,如果出现大量的空方法,将导致系统中产生大量的无用代码,影响代码质量。
另一方面由于客户端针对大接口编程,将在一定程度上破坏程序的封装性,客户端看到了不应该看到的方法,没有为客户端定制接口。因此需要将该接口按照接口隔离原则和单一职责原则进行重构,将其中的一些方法封装在不同的小接口中,确保每一个接口使用起来都较为方便,并都承担某一单一角色,每个接口中只包含一个客户端(如模块或类)所需的方法即可。