您的位置:首页 > 其它

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

2016-12-26 14:06 761 查看
1.接口隔离原则:(Interface Segregation Principle, ISP)

定义:Clients should not be forced to depend upon interfaces that they don't use.(客户端不应该依赖它不需要的接口)。或

    The dependcy of one class to another one should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上)。或

    使用多个专门的接口比使用单一的总接口要好。

2.理解:

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。  在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

单一职责与接口隔离的区别:

单一职责原则注重的是职责;而接口隔离原则注重对接口依赖的隔离。
单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;  而接口隔离原则主要约束接口,主要针对抽象,针对程序整体框架的构建。

3.问题由来:
类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类C来说不是最小接口,则类B和类D必须去实现它们不需要的方法。[解决方案]将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

4.使用ISP的好处:

原则意义上的好处:接口如果能够保持粒度够小,就能保证它足够稳定,正如单一职责原则所飘洋过海榜的那样。(举例:多个专门的接口就好比采用活字制版,可以随时拼版拆版,既利于修改,又利于文字的重用。而单一的总接口就是雕版的印刷,一旦发现错别字,既难改,又需要整块重新雕刻。)
使用多个专门的接口还能够体现对象的层次,因为我们可以通过接口的继承,实现对总接口的定义。(例如,.NET框架中IList接口的定义。)

[csharp]
view plain
copy





public interface IEnumerable    
{    
    IEnumerator GetEnumerator();    
}    
public interface ICollection : IEnumerable    
{    
    void CopyTo(Array array, int index);    
   
    // 其余成员略    
}    
public interface IList : ICollection, IEnumerable    
{    
    int Add(object value);    
    void Clear();    
    bool Contains(object value);    
    int IndexOf(object value);    
    void Insert(int index, object value);    
    void Remove(object value);    
    void RemoveAt(int index);    
   
    // 其余成员略    
}   

如果不采用这样的接口继承方式,而是定义一个总的接口包含上述成员,就无法实现IEnumerable接口、ICollection接口与IList接口成员之间的隔离。假如这个总接口名为IGeneralList,它抹平了IEnumerable接口、ICollection接口与IList接口之间的差别,包含了它们的所有方法。现在,如果我们需要定义一个Hashtable类。根据数据结构的特性,它将无法实现IGeneralList接口。因为Hashtable包含的Add()方法,需要提供键与值,而之前针对ArrayList的Add()方法,则只需要值即可。这意味着两者的接口存在差异。我们需要专门为Hashtable定义一个接口,例如IDictionary,但它却与IGeneralList接口不存在任何关系。正是因为一个总接口的引入,使得我们在可枚举与集合层面上丢失了共同的抽象意义。虽然Hashtable与ArrayList都是可枚举的,也都具备集合特征,它们却不可互换。

如果遵循接口隔离原则,将各自的集合操作功能分解为不同的接口,那么站在ICollection以及IEnumerable的抽象层面上,可以认为ArrayList和Hashtable是相同的对象。在这一抽象层面上,二者是可替换的,如图2-9所示。这样的设计保证了一定程度的重用性与可扩展性。从某种程度来讲,接口隔离原则可以看做是接口层的单一职责原则。



倘若一个类实现了所有的专门接口,从实现上看,它与实现一个总接口的方式并无区别;但站在调用者的角度,不同的接口代表了不同的关注点、不同的职责,甚至是不同的角色。因此,面对需求不同的调用者,这样的类就可以提供一个对应的细粒度接口去匹配。此外,一个庞大的接口不利于我们对其进行测试,因为在为该接口实现Mock或Fake对象 时,需要实现太多的方法。

概括地讲,面向对象设计原则仍然是面向对象思想的体现。例如,单一职责原则与接口隔离原则体现了封装的思想,开放封闭原则体现了对象的封装与多态,而Liskov替换原则是对对象继承的规范,至于依赖倒置原则,则是多态与抽象思想的体现。在充分理解面向对象思想的基础上,掌握基本的设计原则,并能够在项目设计中灵活运用这些原则,就能够改善我们的设计,尤其能够保证可重用性、可维护性与可扩展性等系统的质量属性。这些核心要素与设计原则,就是我们设计的对象法则,它们是理解和掌握设计模式的必备知识。

5.难点:

接口要尽量小(核心定义),但“小”也有限,首先不能违反单一职责原则(接口定义出来是让类来实现的嘛,倘若如此,实现类怎么来SRP?)(去看SRP的7.2节);
接口要高内聚(高内聚:提高接口、类、模块的处理能力,减少对外的交互。例如,不讲任何条件、立刻完成任务的行为就是高内聚的表现),具体到接口隔离原则 ,就是要求在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本;
定制服务,一个系统或系统内的模块之间必然会有耦合,有耦合就要有相互访问的接口,在设计时,就需要为各个访问者定制服务(定制服务就是单独为一个个体提供优良的服务:只提供访问者需要的方法),
e904
本质也是ISP,按需拆分接口;
接口设计是有限度的,但无固化标准。

6.最佳实践:

一个接口只服务于一个子模块或业务逻辑;
通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“筋骨”,而不是“肥嘟嘟”的一大堆方法;
已经被 污染的接口,要尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理;
了解环境,拒绝盲从。

7.范例:
7.1 一个反例(接口臃肿),大意来自3.问题





图1  未遵循ISP的设计图
这个图的意思是:类A依赖接口I中的方法1,2,3;       类C依赖接口I中的方法1,4,5;    类B与类D分别是对类A与类C依赖的实现。   对于类B与类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也 必须要实现这些用不到的方法。代码如下:

[java]
view plain
copy





interface I {  
    public void method1();  
    public void method2();  
    public void method3();  
    public void method4();  
    public void method5();  
}  
class A{  
    public void depend1(I i){  
        i.method1();  
    }  
    public void depend2(I i){  
        i.method2();  
    }  
      
    public void depend3(I i){  
        i.method3();  
    }  
}  
  
class B implements I{  
    public void method1() {  
        System.out.println("类B实现接口I的方法1");  
    }  
    public void method2() {  
        System.out.println("类B实现接口I的方法2");  
    }  
    public void method3() {  
        System.out.println("类B实现接口I的方法3");  
    }  
    //对于类A来说,method4和method5不是必须的,但是由于接口A中有这两个方法,  
    //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。  
    public void method4() {}  
    public void method5() {}  
}  
  
class C{  
    public void depend1(I i){  
        i.method1();  
    }  
    public void depend2(I i){  
        i.method4();  
    }  
    public void depend3(I i){  
        i.method5();  
    }  
}  
  
  
class D implements I{  
    public void method1() {  
        System.out.println("类D实现接口I的方法1");  
    }  
    //对于类C来说,method4和method5不是必须的,但是由于接口A中有这两个方法,  
    //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。  
    public void method2() {}  
    public void method3() {}  
      
    public void method4() {  
        System.out.println("类D实现接口I的方法4");  
    }  
    public void method5() {  
        System.out.println("类D实现接口I的方法5");  
    }  
}  
  
  
public class Client{  
    public static void main(String[] args){  
        A a = new A();  
        a.depend1(new B());  
        a.depend2(new B());  
        a.depend3(new B());  
        C c = new C();  
        c.depend1(new D());  
        c.depend2(new D());  
        c.depend3(new D());   
    }  
}  

可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它们的类有没有用处,实现类中都必须去实现这些方法,这显然是不好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进拆分。在这里我们将原有的接口I拆分为三个接口,拆分后的设计如下所示:



图2 一个遵循ISP的设计图
对应的设计代码如下:

[java]
view plain
copy





interface I1 {  
    public void method1();  
}  
interface I2 {  
    public void method2();  
    public void method3();  
}  
interface I3 {  
    public void method4();  
    public void method5();  
}  
  
class A{  
    public void depend1(I1 i){  
        i.method1();  
    }  
    public void depend2(I2 i){  
        i.method2();  
    }  
    public void depend3(I2 i){  
        i.method3();  
    }  
}  
class B implements I1, I2{  
    public void method1() {  
        System.out.println("类B实现接口I1中的方法1");  
    }  
    public void method2() {  
        System.out.println("类B实现接口I2中的方法2");  
    }  
    public void method3() {  
        System.out.println("类B实现接口I2中的方法3");  
    }  
}  
class C{  
    public void depend1(I1 i){  
        i.method1();  
    }  
    public void depend2(I3 i){  
        i.method4();  
    }  
    public void depend3(I3 i){  
        i.method5();  
    }  
}  
class D implements I1, I3{  
    public void method1() {  
        System.out.println("类D实现接口I1中的方法1");  
    }  
    public void method4() {  
        System.out.println("类D实现接口I3中的方法4");  
    }  
    public void method5() {  
        System.out.println("类D实现接口I3中的方法5");  
    }  
}  

7.2 一个在需求变化中,才发现接口粒度过大的例子:
星控找美女的过程:





图3  初步的星探找美女图类,美女必须 长得好看、身材好、有气质

但是随着人们审美水品的不断提升,人们对气质美女也产生了很大的认同感,即不太要求长相与身材,这时,新的类图如下:(实为7.1节的演化版)





图4 新类图,如果一开始能做到此,便能防患于未然
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  设计模式