目录
1:PDF上传链接
这个原则用来处理"胖(fat)"接口所具有的缺点。如果类的接口不是内聚的(cohesive),就表示该类具有"胖"的接门。换句话说,类的"胖"接口可以分解成多组方法。每一组方法都服务于一组不同的客户程序。这样,一些客户程序可以使用一组成员函数,而其他客户程序可以使用其他组的成员函数。
ISP承认存在有一些对象,它们确实不需要内聚的接口:但是SP建议客户程序不应该看到它们作为单一的类存在。相反,客户程序看到的应该是多个具有内聚接口的抽象基类。
12.1:接口污染
考虑一个安全系统。在这个系统中,有一些Door对象,可以被加锁和解锁,并且Doo对象知
道自己是开着还是关着。(参见程序12.1。)
cpp
程序12.1安全系统中的Door
class Door
{
public:
virtual void Lock() = 0:
virtual void Unlock() = 0;
virtual bool IsDoorOpen() = 0:
}
该类是抽象的,这样客户程序就可以使用那些符合Doo接口的对象,而不需要依赖于Doo的
特定实现。
现在,考虑一个这样的实现,TimedDoor,.如果门开着的时间过长,它就会发出警报声。为了做到这一点,TimedDoor对象需要和另一个名为Timer的对象交互。(参见程序12.2。)】
cpp
程序12.2
class Timer
{
public:
void Register(int timeout, Timerclient *client);
};
class Timerclient
{
public:
virtual void Timeout() = 0;
};
如果一个对象希望得到超时通知,它可以调用Timer的Register函数。该函数有两个参数,一个是超时时间,另个是指向TimerClient对象的指针,该对象的TimeOut函数会在超时到达时被调用。
我们怎样将TimerClient类和TimedDoor类联系起来,才能在超时时通知到TimedDoor中相应的处理代码呢?有几个方案可供选择。图12.1中展示了一个易想到的解决方案。其中D00r继承了TimerClient,因此TimedDoor也就继承了TimeClient。这就保证了TimerClient可以把自己注册到Timer中,并且可以接收TimeOut消息。
图12.1
虽然这个解决方案很常见,但是它也不是没有问题。最主要的问题是,现在Door类依赖于TimerClient了。可是并不是所有种类的Doo都需要定时功能。事实上,最初的Do0抽象类和定时功能没有任何关系。如果创建了无需定时功能的Door的派生类,那么在这些派生类中就必须要提供TimeOut方法的退化(degenerate)实现这就有可能违反LSP。此外,使用这些派生类的应用程序即使不使用TimerClient类的定义,也必须要引入(import)它。这样就具有了不必要的复杂性以及不必要的重复的臭味。
这是一个接口污染的例子,这种情况在像C+、Java这样的静态类型语言中是很常见的。Door的接口被一个它不需要的方法污染了。在D00的接口中加入这个方法只是为了能给它的一个子类带来好处。如果持续这样做的话,那么每次子类需要一个新方法时,这个方法就会被加到基类中去。
这会进一步污染基类的接口,使它变"胖"。
此外,每次基类中加入一个方法时,派生类中就必须要实现这个方法(或者定义一个缺省实现)。事实上,有一种特定的相关实践,可以使派生类无需实现这些方法,该实践的做法是把这些接口合并为一个基类,并在这个基类中提供接口中方法的退化实现。但是按照我们前面所学习的,这种实践违反了LSP,带来了维护和重用方面的问题。
12.2:分离客户就是分离接口
Door接口和TimerClient接口是被完全不同的客户程序使用的。Timer使用TimerClient,.而操作门的类使用D00。既然客户程序是分离的,所以接口也应该保持分离。为什么呢?因为客户程序对它们使用的接口施加有作用力。
12.3:接口隔离原则(ISP)
不应该强迫客户依赖于它们不用的方法。
如果强迫客户程序依赖于那些它们不使用的方法,那么这些客户程序就面临着由于这些未使用方法的改变所带来的变更。这无意中导致了所有客户程序之间的耦合。换种说法,如果一个客户程序依赖于个含有它不使用的方法的类,但是其他客户程序却要使用该方法,那么当其他客户要求这个类改变时,就会影响到这个客户程序。我们希望尽可能地避免这种耦合,因此我们希望分离接口。
12.4:类接口与对象接口
再次考虑-一下TimedDoor问题。这里有一个具有两个独立的接口,由两个独立的客户一Timer以及Doo所使用的对象。因为实现这两个接口需要操作同样的数据,所以这两个接口必须在同~个对象中实现。那么怎样才能遵循1SP呢?怎样才能分离必须在起实现的接口呢?
该问题的答案基于这样的事实,就是一个对象的客户不是必须通过该对象的接口去访问它,也可以通过委托或者通过该对象的基类去访问它。
12.4.1:使用委托分离接口
一个解决方案是创建一个派生自TimerClient的对象,并把对该对象的请求委托给TimedDoor。图12.2展示了这个解决方案。
当TimedDoor想要向Timer对象注册一个超时请求时,它就创建-个DoorTimerAdapter并且把它注册给Timer。当Timer对象发送TimeOut消,息给DoorTimerAdapter时,DoorTimerAdapter把这个消息委托给TimedDoor。
图12.2 Door定时器适配器
这个解决方案遵循ISP原则,并且避免了Door的客户程序和Timr之间的耦合。TimedDoor也不必具有和TimerClient一样的接口。DoorTimerAdapter会将TimerClient接口转换成TimedDoor接口。因此,这是一个非常通用的解决方案。(参见程序12.4。)
cpp
程序12.4 TimedDoor.cpp
class TimedDoor: public Door
{
public:
virtual void DoorTimeOut (int timeOutID);
}:
class DoorTimeAdapter: public Timerclient
{
public:
DoorTimerAdapter (TimedDoor& theDoor): itsTimedDoor (theDoor)
{}
vistual void TimeOut (int timeoutId)
{
itsTimedDoor.DoorTimeout (timeOutId);
}
private:
TimedDoor&itsTimedDoor;
}
不过,这个解决方案还是有些不太优雅。每次想去注册一个超时请求时,都要去创建一个新的对象。此外,委托处理会导致一些很小但仍然存在的运行时间和内存的开销。有一些应用领域,比如嵌入式实时控制系统,其中内存和运行时间都是非常宝贵的,以至于这种开销成了一个值得关注的问题。
12.4.2:使用多重继承分离接口
图12.3和程序12.5展示了如何使用多重继承来达到符合ISP的目标。在这个模型中,TimerdDoor同时继承了Door和TimerClient。.尽管这两个基类的客户程序都可以使用TimedDoor,但是实际上却都不再依赖于TimedDoor类。这样,它们就通过分离的接口使用同一个对象。
图12.3多重继承的Timed Door
java
程序12,5 TimedDoor.cpp
class TimedDoor: public Door, pubiic Timerclient
{
public:
virtual void DoorTimeOut (int timeOutID);
}
通常会优先选择这个解决方案。只有当DoorTimerAdapter对象所做的转换是必须的,或者不同的时候会需要不同的转换时,我才会选择图12.2中的方案而不是图12.3中的方案。
结论
胖类(fat class)会导致它们的客户程序之间产生不正常的并且有害的耦合关系。当一个客户程序要求该胖类进行一个改动时,会影响到所有其他的客户程序。因此,客户程序应该仅仅依赖于它们实际调用的方法。通过把胖类的接口分解为多个特定于客户程序的接口,可以实现这个目标。每个特定于客户程序的接口仪仅声明它的特定客户或者客户组调用的那些函数。接着,该胖类就可以继承所有特定于客户程序的接口,并实现它们。这就解除了客户程序和它们没有调用的方法间的依赖关系,并使客户程序之间互不依赖。