接口隔离原则(ISP
): 不应该强迫客户依赖于它们不用的方法。
这个原则用来拆分庞大臃肿的接口成为更小的和更具体的接口,这样使用者将会只需要知道他们感兴趣的方法。这种缩小的接口也被称为角色接口。ISP
承认存在有一些对象,它们确实不需要内聚的接口,但是ISP
建议不应该把它们作为一个单一的类存在,相反,使用者看到的应该是多个具有内聚接口的抽象基类。
接口隔离原则的目的是让系统解耦,从而更容易重构、更改和重新部署。
考虑一个安全系统,在这个系统中,有一些Door
对象,可以被加锁和解锁,并且Door
对象知道自己是开着还是关着。现在,考虑一个这样的实现,TimedDoor
,如果门开着的时间过长,它就会发出警报声。
public interface Door {
void lock();
void unlock();
boolean isDoorOpen();
}
如果一个对象希望得到超时通知,它可以调用Timer
的register
函数。该函数有两个参数,一个是超时时间,另一个是指向TimerClient
对象的引用,该对象的timeout
函数会在超时到达时被调用。
public class Timer {
public void register(int timeout, TimerClient client) {
}
}
public interface TimerClient {
void timeout();
}
如何将TimerClient
类和TimedDoor
类联系起来,才能在超时时通知到TimedDoor
中相应的处理代码呢?下面是一个容易想到的解决方案:
其中,Door
继承了TimerClient
,因此TimedDoor
也就继承了TimerClient
。这就保证了TimerClient
可以把自己注册到Timer
中,并且可以接收timeout
消息。
这个方案也不是没有问题,主要的问题是,现在Door
类依赖于TimerClient
了。可并不是所有种类的Door
都需要定时功能。实际上,最初的Door
抽象类和定时功能没有任何关系。如果创建了无需定时功能的Door
的派生类,那么在这些派生类中就必须要提供timeout
方法的退化实现(这就有可能违反了LSP
)。此外,使用这些派生类的应用程序即使不使用TimerClient
类的定义,也必须要引入它,这样就有了不必要的复杂性和不必要的耦合。
上面的设计方案是一个典型的接口污染的例子,Door
的接口被一个它不需要的方法污染了。在Door
的接口中加入这个方法只是为了能给它的一个子类带来好处。如果每次子类需要一个新方法时,这个方法就加到基类中去,这会进一步污染基类的接口。
而且,每次基类中加入一个方法时,派生类中就必须要实现这个方法(或者定义一个缺省实现)。事实上,有一种特定的实践让我们可以使派生类无需实现这些方法,该实践的做法是把这些接口合并为一个基类,并在这个基类中提供接口中方法的退化方法。但是,这种实践违反了LSP
,带来了维护和重用方面的问题。
Door
接口和TimerClient
接口是被完全不同的客户程序使用的。Timer
使用TimerClient
,而操作门的类使用Door
。既然客户程序是分离的,所以接口也应该保持分离。
为什么呢?
我们在考虑软件中引起变化的原因时,通常考虑的都是接口的变化会怎样影响它们的使用者。例如,如果TimerClient
的接口改变了,我们会去关心TimerClient
的所有使用者要做什么样的改变。
然而,实际上也存在着反向的影响,有时,迫使接口改变的,正是它们的使用者。例如,有些Timer
的使用者会注册多个超时通知请求。比如对于TimerDoor
来说,当它检测到门被打开时,会向Timer
发送一个register
消息,请求一个超时通知。可是,在超时到达前,门关上了,关闭一会儿后又被再次打开。这就导致在原先的超时到达前又注册了一个新的超时请求。最后,最初的超时到达,TimedDoor
的timeout
方法被调用,Door
错误地发出了警报。
我们可以通过在注册时增加一个timeOutID
的唯一标识来改正上面情形中的错误。
public class Timer {
public void register(int timeout, int timeOutID, TimerClient client) {
}
}
public interface TimerClient {
void timeout(int timeOutID);
}
显然,这个改变会影响到TimerClient
的所有使用者。对于上图中的设计,这个改变还会影响到Door
以及Door
的所有使用者,这不是我们所期望的。如果程序中一部分的更改会影响到程序中完全和它无关的其他部分,那么更改的代价和影响就变得不可预测,并且更改所附带的风险也会急剧增加。
如果强迫客户程序依赖于那些它们不使用的方法,那么这些客户程序就面临着由于这些未使用方法的改变所带来的变更。因此,我们希望分离接口来尽可能的避免这种耦合。
再考虑TimedDoor
的问题,这里有两个独立的接口,有两个独立的客户--Timer
以及Door
所使用的对象。因为实现这两个接口需要操作同样的数据,所以这两个接口必须在同一个对象中实现。那么怎样才能遵循ISP
呢?如何才能分离必须在一起实现的接口呢?
该问题的答案是:一个对象的客户(使用者)不是必须通过该对象的接口去访问它,也可以通过委托或者通过该对象的基类去访问它。
一个解决方案就是创建一个派生自TimerClient
的对象,并把对该对象的请求委托给TimedDoor
。如下图所示:
当TimedDoor
想要向Timer
对象注册一个超时请求时,它就创建一个DoorTimerAdapter
并且把它注册给Timer
。当Timer
对象发送timeout
消息给DoorTimerAdapter
时,DoorTimerAdapter
把这个消息委托给TimedDoor
。
这个方案遵循ISP
原则,并且避免了Door
的客户程序和Timer
之间的耦合。同时,Timer
的变化不会影响到任何Door
的使用者。此外,TimedDoor
也不必具有和TimerClient
一样的接口。DoorTimerAdapter
会将TimerClient
接口转换成TimedDoor
接口。
这个解决方案还是有些不够优雅,每次想去注册一个超时请求时,都要去创建一个新的对象。此外,委托处理会导致一些很小但仍然存在的运行时间和内存的开销,在某些应用领域,内存和运行时间都非常宝贵,以至于这种开销成了一个值得关注的问题。
还有一种解决方案是通过分离接口来实现。如下图所示:
在这个模型中,TimedDoor
同时继承了Door
和TimerClient
。尽管这两个基类的使用者都可以使用TimedDoor
,但是实际上却都不再依赖于TimedDoor
类。这样,它们就通过分离的接口使用同一个对象。
通常我们会优先选择这个解决方案。
再看一个ATM
用户界面的例子:ATM
需要灵活的用户界面,它需要根据不同的操作显示不同的界面,比如输出信息需要能显示为多种语言等。显然,通过创建一个抽象基类把一些公共的逻辑封装起来,然后把每个ATM
能执行的不同操作封装为类Transaction
的派生类,这样我们可以得到类DepositTransaction
、WithdrawalTransaction
以及TransferTransaction
。每个类都调用UI
的方法,设计如下图示:
现在我们用OOD
的设计原则来检验上面的设计,显然这个设计不符合LSP
原则。每个操作所使用的UI
的方法,其他的操作类都不会使用,因此,对于任何一个Transaction
的派生类的改动都会迫使对UI
的相应改动,同时也影响到了其他所有Transaction
的派生类及其他所有依赖于UI
接口的类,引入了依赖的传递性和不必要的耦合。
例如,如果要增加一种操作PayGasBillTransaction
,为了处理该操作想要显示的特定消息,就必须要在UI
中加入新的方法。但由于DepositTransaction
、WithdrawalTransaction
以及TransferTransaction
全都依赖于UI
接口,所以它们都需要重新编译。更糟糕的是,如果这些操作是作为组件供其他组件使用的话,那么这些组件必须全部重新编译部署,但事实上它们的逻辑没有做过任何的改动。
通过将UI
接口分解成DepositUi
、WithdrawUI
以及TransferUI
这样的单独的接口,可以避免这种不合适的耦合,最终的UI
接口可以去实现这些单独的接口:
每次创建一个Transaction
类的新派生类时,抽象接口UI
就需要增加一个相应的基类,并且UI
接口以及所有它的派生类都必须改变。不过,这些类并没有被很多的组件使用,事实上,它们可能仅被main
程序或那些启动系统并创建具体UI
实例所使用,因此,增加新的UI
基类所带来的影响是可控的。
臃肿的类(胖类)会导致它们的使用者之间产生不必要的耦合关系,当一个使用者要求胖类进行一个改动时,会影响到所有其他的使用者。因此,使用者应该仅仅依赖于它们实际调用的方法。通过把胖类的接口分解为多个特定于使用者的接口,可以实现这个目标。每个特定于使用者的接口仅仅声明它的特定客户或者客户组调用的那些函数。接着,该胖类就可以继承所有特定于使用者的接口,并实现它们,这样就解除了使用者和它们没有调用的方法之间的依赖关系,并使使用者之间互不依赖。
OOD
的每个原则在应用时都必须小心,不能过度使用它们。尤其是在一些复杂的场景中,需要结合实际的需求做出一些平衡。