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

来源:互联网 发布:java的数组怎么定义 编辑:程序博客网 时间:2024/05/29 04:53

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.理解:

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

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

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

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

4.使用ISP的好处:
  1. 原则意义上的好处:接口如果能够保持粒度够小,就能保证它足够稳定,正如单一职责原则所飘洋过海榜的那样。(举例:多个专门的接口就好比采用活字制版,可以随时拼版拆版,既利于修改,又利于文字的重用。而单一的总接口就是雕版的印刷,一旦发现错别字,既难改,又需要整块重新雕刻。)
  2. 使用多个专门的接口还能够体现对象的层次,因为我们可以通过接口的继承,实现对总接口的定义。(例如,.NET框架中IList接口的定义。)
    [csharp] view plain copy 在CODE上查看代码片派生到我的代码片
    1. public interface IEnumerable    
    2. {    
    3.     IEnumerator GetEnumerator();    
    4. }    
    5. public interface ICollection : IEnumerable    
    6. {    
    7.     void CopyTo(Array array, int index);    
    8.    
    9.     // 其余成员略    
    10. }    
    11. public interface IList : ICollection, IEnumerable    
    12. {    
    13.     int Add(object value);    
    14.     void Clear();    
    15.     bool Contains(object value);    
    16.     int IndexOf(object value);    
    17.     void Insert(int index, object value);    
    18.     void Remove(object value);    
    19.     void RemoveAt(int index);    
    20.    
    21.     // 其余成员略    
    22. }   

如果不采用这样的接口继承方式,而是定义一个总的接口包含上述成员,就无法实现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方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本;
  • 定制服务,一个系统或系统内的模块之间必然会有耦合,有耦合就要有相互访问的接口,在设计时,就需要为各个访问者定制服务(定制服务就是单独为一个个体提供优良的服务:只提供访问者需要的方法),本质也是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 在CODE上查看代码片派生到我的代码片
  1. interface I {  
  2.     public void method1();  
  3.     public void method2();  
  4.     public void method3();  
  5.     public void method4();  
  6.     public void method5();  
  7. }  
  8. class A{  
  9.     public void depend1(I i){  
  10.         i.method1();  
  11.     }  
  12.     public void depend2(I i){  
  13.         i.method2();  
  14.     }  
  15.       
  16.     public void depend3(I i){  
  17.         i.method3();  
  18.     }  
  19. }  
  20.   
  21. class B implements I{  
  22.     public void method1() {  
  23.         System.out.println("类B实现接口I的方法1");  
  24.     }  
  25.     public void method2() {  
  26.         System.out.println("类B实现接口I的方法2");  
  27.     }  
  28.     public void method3() {  
  29.         System.out.println("类B实现接口I的方法3");  
  30.     }  
  31.     //对于类A来说,method4和method5不是必须的,但是由于接口A中有这两个方法,  
  32.     //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。  
  33.     public void method4() {}  
  34.     public void method5() {}  
  35. }  
  36.   
  37. class C{  
  38.     public void depend1(I i){  
  39.         i.method1();  
  40.     }  
  41.     public void depend2(I i){  
  42.         i.method4();  
  43.     }  
  44.     public void depend3(I i){  
  45.         i.method5();  
  46.     }  
  47. }  
  48.   
  49.   
  50. class D implements I{  
  51.     public void method1() {  
  52.         System.out.println("类D实现接口I的方法1");  
  53.     }  
  54.     //对于类C来说,method4和method5不是必须的,但是由于接口A中有这两个方法,  
  55.     //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。  
  56.     public void method2() {}  
  57.     public void method3() {}  
  58.       
  59.     public void method4() {  
  60.         System.out.println("类D实现接口I的方法4");  
  61.     }  
  62.     public void method5() {  
  63.         System.out.println("类D实现接口I的方法5");  
  64.     }  
  65. }  
  66.   
  67.   
  68. public class Client{  
  69.     public static void main(String[] args){  
  70.         A a = new A();  
  71.         a.depend1(new B());  
  72.         a.depend2(new B());  
  73.         a.depend3(new B());  
  74.         C c = new C();  
  75.         c.depend1(new D());  
  76.         c.depend2(new D());  
  77.         c.depend3(new D());   
  78.     }  
  79. }  
可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它们的类有没有用处,实现类中都必须去实现这些方法,这显然是不好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进拆分。在这里我们将原有的接口I拆分为三个接口,拆分后的设计如下所示:
图2 一个遵循ISP的设计图
对应的设计代码如下:
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. interface I1 {  
  2.     public void method1();  
  3. }  
  4. interface I2 {  
  5.     public void method2();  
  6.     public void method3();  
  7. }  
  8. interface I3 {  
  9.     public void method4();  
  10.     public void method5();  
  11. }  
  12.   
  13. class A{  
  14.     public void depend1(I1 i){  
  15.         i.method1();  
  16.     }  
  17.     public void depend2(I2 i){  
  18.         i.method2();  
  19.     }  
  20.     public void depend3(I2 i){  
  21.         i.method3();  
  22.     }  
  23. }  
  24. class B implements I1, I2{  
  25.     public void method1() {  
  26.         System.out.println("类B实现接口I1中的方法1");  
  27.     }  
  28.     public void method2() {  
  29.         System.out.println("类B实现接口I2中的方法2");  
  30.     }  
  31.     public void method3() {  
  32.         System.out.println("类B实现接口I2中的方法3");  
  33.     }  
  34. }  
  35. class C{  
  36.     public void depend1(I1 i){  
  37.         i.method1();  
  38.     }  
  39.     public void depend2(I3 i){  
  40.         i.method4();  
  41.     }  
  42.     public void depend3(I3 i){  
  43.         i.method5();  
  44.     }  
  45. }  
  46. class D implements I1, I3{  
  47.     public void method1() {  
  48.         System.out.println("类D实现接口I1中的方法1");  
  49.     }  
  50.     public void method4() {  
  51.         System.out.println("类D实现接口I3中的方法4");  
  52.     }  
  53.     public void method5() {  
  54.         System.out.println("类D实现接口I3中的方法5");  
  55.     }  
  56. }  


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


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

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


图4 新类图,如果一开始能做到此,便能防患于未然
0 0
原创粉丝点击