论敏捷设计中的接口隔离原则ISP

posted at 2024.8.14 16:00 by 风信子

使用任何语言在程序开发的过程中,敏捷设计都是非常重要的。幸运的是,有一些基本的原则可以帮助我们在设计程序时避免一些常见的陷阱和错误,使设计更便捷、高效。下面是公认的程序敏捷设计的七大原则: 

设计原则名称   简称            核心思想

单一职责原则    SRP    一个类只负责一个特定职责

开放封闭职责    OCP    软件实体应该可以扩展,但不应该修改其代码

接口隔离原则    ISP    使用多个专门的接口,而不是一个通用的接口

依赖倒置原则    DIP    高层模块不应该依赖低层模块,抽象不应该依赖细节

里氏替换原则    LSP    任何基类可以出现的地方,子类也可以出现

迪米特法则      LoD    一个对象应尽量少地了解其他对象,降低耦合度

合成复用原则    CRP    优先使用组合而不是继承来实现代码复用

其中,第三条原则ISP:接口隔离原则是用来处理“胖”接口所存在的缺点。如果类的接口不是内聚的,我们称该接口是“胖”接口。换句话说,类的“胖”接口可以分解成多组方法,每一组方法都服务于一组不同的客户程序,这样,一些客户程序可以使用一组成员函数,而其他客户程序可以使用其他组的成员函数。

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

一、避免接口污染 

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

安全系统中Door的C#代码

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

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

     C#代码如下:

                    public class Timer{
public void Register(int timeout,TimerClient client)
{/*省略的代码*/}
}
public interface TimerClient{
    void TimeOut();
}

如果一个对象希望得到超时通知,就可以调用Timerregister函数。

那么,我们怎样得到将TimerClient类和TimedDoor类联系起来,才能在超时时通知到TimedDoor中相应的处理代码呢?有几个方案可供选择,一个常见的解决方案是Door继承TimerClient,因此TimedDoor也就继承了TimerClient。这就保证了TimerClient可以把自己注册到Timer中并且可以接收TimeOut消息。

这种做法的问题是,现在Door类依赖于TimerClient了,可是并不是所有种类的Door都需要定时功能。

这就是一个接口污染的例子。在Door的接口中加入这个方法只是为了能给它的子类带来好处,如果持续这样做的话,会进一步污染基类的接口,使它变“胖”。

   二、不应该强迫客户程序依赖并未使用的方法

         ISP之一是不应该强迫客户程序依赖并未使用的方法。例如,有些Timer的使用者会注册多个超时通知要求。比如对于TimedDoor来说,当它检测到门打开时,会向Timer发送一个Register消息,请求一个超时通知,可是,在超时到达前,门关上了,关闭一会儿后又被再次打开。这就导致在原先的超时到达前又注册了一个新的超时请求。最后,最初的超时到达,TimedDoor的TimeOut 方法被调用,Door错误地发出了警报。 

使用下述清单展示的代码,可以改正上面情形中的错误。在每次超时注册中都包含一个唯一的标识码,并在调用方法时再次使用该标识码。

使用IDTimer类的C#代码:

             public class Timer{

public void Register(int timeout,int timeOutId,TimerClient client)
{/*省略的代码*/}
}
public interface TimerClient{
    void TimeOut(int timeOutID);
} 

所以说,如果强迫客户程序依赖于那些它们不使用的方法,那么这些客户程序就面临着由于这些未使用方法的改变所带来的变更。这无意中导致了所有客户程序之间的耦合,我们希望尽可能地避免这种耦合,分离接口。

    三、分离多个接口

         再次考察一下TimedDoor,它具有两个独立的接口Timer和Door,这两个接口必须在同一个对象中实现那么怎样才能遵循ISP呢?

    1)使用委托分离接口

    一个解决方案是创建一个派生TimerClient的对象,并把对该对象的请求委托给TimedDoor。

     C#代码如下:

public interface TimedDoorDoor{
        void DoorTimeOut(int timeOutId);
}
public class DoorTimerAdapter:TimerClient{
private TimedDoor timeDoor;
public DoorTimerAdapter(TimeDoor theDoor){
timeDoor=theDoor;
}
                    public virtual void TimeOut(int timeOutId){
                           timedDoor.DoorTimeOut(timeOutId);
                    }

      这个解决方案遵循ISP,避免了Door的客户程序和Timer之间的耦合,是一个非常通用的解决方案。不过,在一些应用领域如嵌入式实时控制系统,其内存是非常宝贵的,上述代码存在较大的内存开销是一个值得关注的问题。

    2)使用多重继承分离多个接口

    另一个解决方案是TimedDoor同时继承Door和TimerClient。尽管这两个基类的客户程序都可以使用TimedDoor,但是实际上都不再依赖于它。这样,它们就通过分离的接口使用同一个对象。

public interface TimedDoor:Door,TimerClient{

 

}

Tags: , , , , , , ,

IT技术

添加评论

  Country flag

biuquote
  • 评论
  • 在线预览
Loading