敏捷软件开发 – ISP 接口隔离原则

Posted 顾强

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了敏捷软件开发 – ISP 接口隔离原则相关的知识,希望对你有一定的参考价值。

  如果类的接口不是内聚的,就表示该类具有“胖”接口。换句话说,类的“胖”接口可以分解成多组方法。每一组方法服务于一组不同的客户程序。

  ISP承认有一些对象确实需要有非内聚的接口,但是ISP建议客户程序不应该看到它们作为单一的类存在。相反,客户程序看到的应该是多个具有内聚接口的抽象基类。

接口污染

  考虑一个安全系统。在这个系统中,有一些Door对象,可以被加锁和解锁,并且Door对象知道自己是开着还是关着。这个Door编码成一个接口,这样客户程序就可以使用那些符合Door接口的对象,而不需要依赖于Door的特定实现。

public interface Door
{
    void Lock();
    void Unlock();
    bool IsDoorOpen();
}

  现在,考虑一个这样的实现,TimedDoor,如果门开着的时间过长,它就会发出警报声。为了实现这一点,TimedDoor对象需要和另外一个名为Timer的对象交互。

public class Timer
{
    public void Register(int timeout,TimerClient client)
    {}
}

public interface TimerClient
{
    void TimeOut();
}

  如果一个对象希望得到超时通知,它可以调用Timer的Register函数。该函数有两个参数,一个是超时时间,另一个是指向TimerClient对象的引用,其TimeOut函数会在超市到达时被调用。

  如何把TimerClient类和TimedDoor类联系起来,才能在超时时通知TimedDoor中相应的处理代码呢?

  常见的解决方案是,其中Door继承了TimerClient,因此TimedDoor也就继承了TimeClient。这就保证了TimerClient可以把自己注册到Timer中,并且可以接收TimeOut消息。

  这种做法的问题是,现在Door依赖于TimerClient了。可是并不是所有种类的Door都需要定时功能。事实上,最初的Door抽象类和定时功能没有任何关系。如果创建了无需定时功能的Door的派生类,那么在这些派生类中就必须要提供TimeOut方法的退化实现,这就可能违反ISP。此外,使用这些派生类的应用程序及时不使用TimerClient类的定义,也必须要引入它。这样就具有了不必要的复杂性以及不必要的重复性的臭味。

分离客户就是分离接口

  Door接口和TimerClient接口是被完全不同的客户程序使用的。Timer使用TimerClient,而操作门的类使用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。这样,它们就通过分离的接口使用同一对象。

  通常都会优先使用这个解决方案。

结论

  胖类会导致它们的客户程序之间产生不正常的并且有害的耦合关系。当一个客户程序要求该胖类进行一个改动时,会影响到所有其他的客户程序。因此,客户程序应该仅仅依赖于它们实际调用的方法。通过把胖类的接口分解为多个特定于客户程序的接口,可以实现这个目标。每个特定于客户程序的接口仅仅声明它的特定客户或者客户组调用的那些函数。接着,该胖类就可以继承所有特定于客户程序的接口,并实现它们。这就解除了客户程序和它们没有调用的方法间的依赖关系,并使客户程序之间互不依赖。

 

 

摘录自:[美]RobertC.Martin、MicahMartin著,邓辉、孙鸣译 敏捷软件开发原则、模式与实践(C#版修订版) [M]、人民邮电出版社,2013、115-121、

以上是关于敏捷软件开发 – ISP 接口隔离原则的主要内容,如果未能解决你的问题,请参考以下文章

设计原则-ISP接口隔离原则

设计模式 - 六大设计原则之ISP(接口隔离原则)

设计模式 - 六大设计原则之ISP(接口隔离原则)

设计模式 - 六大设计原则之ISP(接口隔离原则)

设计原则之-接口隔离原则(Interface Segregation Principle, ISP)

面向对象的六大原则之 接口隔离原则——ISP