接口隔离原则ISP

56 阅读5分钟

接口隔离原则是什么

角色的合理划分

将“接口”理解为一个类所提供的所有方法的特征集合,也就是一种在逻辑上才存在的概念。这样的话,接口的划分就直接带来类型的划分。

一个接口相当于剧本中的一种角色,而此角色在一个舞台上由哪一个演员来演则相当于接口的实现。因此,一个接口应当简单地代表一个角色,而不是多个角色。如果系统涉及到多个角色的话,那么每一个角色都应当由一个特定的接口代表。

定制服务

将接口理解成为侠义的 Java 接口,这样一来,接口隔离原则讲的就是为同一个角色提供宽、窄不同的接口,以对付不同的客户端,这种办法再服务行业中叫做定制服务(Customized Service),在以下示意类图中,有一个角色 Service 以及三个不同的客户端。这三个客户端需要的服务都是稍稍不同的,因此系统分别为它们提供了三个不同的 Java 接口,即 IService1,IService2 和 IService3。显然,每一个 Java 接口都仅仅将客户端需要的行为暴露给客户端,而没有将客户端不需要的行为放到接口中,这也是适配器模式的应用。

image.png

接口污染

过于臃肿的接口是对接口的污染。

由于每一个接口都代表一个角色,实现一个接口的对象,在它的整个生命周期中都扮演这个角色,因此将角色区分清楚就是系统设计的一个重要工作。因此,一个符合逻辑的推断,不应当将几个不同的角色都交给同一个接口,而应当交给不同的接口。

一个没有经验的设计师往往想节省接口的数目,因此,将一些看上去差不多的接口合并。一些人将这看做是代码优化的一部分,这是错误的。

准确而恰当地划分角色以及角色所对应的接口,是面向对象的设计的一个重要的组成部分。将没有关系的接口合并在一起,形成一个臃肿的大接口,对对角色和接口的污染。

与迪米特法则的关系

迪米特法则要求任何一个软件实体,除非绝对需要,不然不要与外界通信。即使必须进行通信,也应当尽量限制通信的广度和深度。

显然,定制服务原则拒绝向客户端提供不需要提供的行为,是符合迪米特法则的。

如何做到接口隔离原则

UML类图

image.png

示例代码

package principle;

/**
 * 接口隔离
 *
 * @author asyyr
 */
public class Segregation {
    public static void main(String[] args) {
        C c = new C();
        c.depend1(new A());
        c.depend2(new A());

        D d = new D();
        d.depend1(new B());
        d.depend4(new B());
    }
}

interface Interface1 {
    void operation1();
}

interface Interface2 {
    void operation2();
    void operation3();
}

interface Interface3 {
    void operation4();
    void operation5();
}

class A implements Interface1, Interface2 {
    @Override
    public void operation1() {
        System.out.println("A operation1");
    }

    @Override
    public void operation2() {
        System.out.println("A operation2");
    }

    @Override
    public void operation3() {
        System.out.println("A operation3");
    }
}

class B implements Interface1, Interface3 {
    @Override
    public void operation1() {
        System.out.println("B operation1");
    }

    @Override
    public void operation4() {
        System.out.println("B operation4");
    }

    @Override
    public void operation5() {
        System.out.println("B operation5");
    }
}

class C {
    public void depend1(Interface1 i) {
        i.operation1();
    }

    public void depend2(Interface2 i) {
        i.operation2();
    }

    public void depend3(Interface2 i) {
        i.operation3();
    }
}

class D {
    public void depend1(Interface1 i) {
        i.operation1();
    }

    public void depend4(Interface3 i) {
        i.operation4();
    }

    public void depend5(Interface3 i) {
        i.operation5();
    }
}

在设计模式的体现

备忘录模式

备忘录模式是在不破坏封装的条件下,捕捉一个对象的状态,并将之外部化,从而可以在将来合适的时候把这个对象还原到存储起来的状态。备忘录模式的简略类图如下图所示。

image.png

在这里,不破坏封装是一个关键词,为了做到这一点,必须使备忘录对象向外界提供双重接口,也即一个窄接口和一个宽接口。

宽接口是为发起人角色准备的,因为这个备忘录角色所存储的状态就是属于这个发起人角色的,而且这个角色需要访问备忘录角色所存储的信息以便恢复自己的状态。

窄接口是为包括负责人角色在内的所有其他对象准备的,因为它们不需要、也不应该读取备忘录角色所存储的信息。

换言之,发起人角色和负责人角色就相当于备忘录角色的不同客户端,而这种为不同客户端提供不同接口的做法就是定制服务概念的体现。

迭代子模式

迭代子模式提供一个迭代子对象,使得客户端可以顺序地访问一个聚集中的元素,而不必暴露聚集的内部表象。迭代子模式的示意图如下图所示。

image.png

换言之,上面的这个系统的客户端和系统内部的迭代子对象都需要访问聚集对象,但是它们所需要的访问性质有所不同。前者仅需要通过一个迭代子接口遍历聚集的元素,而迭代子对象则需要知道聚集对象的内部结构信息。

因此,聚集对象向不同的客户端提供了不同的接口,一个是宽接口,提供给迭代子对象;另一个是窄接口,提供给系统的客户端。