接口隔离原则的理解

ISP

接口隔离原则(Interface Segregation Principle,ISP)要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。

ISP 定义拓展

一个类对另一个类的依赖应该建立在最小的接口上(The dependency of one class to another one should depend on the smallest possible interface)。

总结上述含义为:要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

ISP vs LSP

接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:

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

ISP 理解

接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。

  1. 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
  2. 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
  3. 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
  4. 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
  5. 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。

在具体应用接口隔离原则时,应该根据以下几个规则来衡量。

  • 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
  • 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
  • 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

Golang Demo

Bad ISP

不好的接口设计, 接口方法很多, 比较臃肿, 需要实现接口时负担很重

package interface_segregation

type IBadAnimal interface {
    ID() int
    Name() string

    Eat() error
    Fly() error
    Swim() error
}

Bad ISP bird instance

BadBird实现了IBadAnimal接口.
BadBird是不支持Swim()的, 但由于接口要求, 只能返回无意义的错误应付.

package interface_segregation

import (
    "errors"
    "fmt"
)

type BadBird struct {
    iID int
    sName string
}

func NewBadBird(id int, name string) IBadAnimal {
    return &BadBird{
        iID: id,
        sName: name,
    }
}

func (me *BadBird) ID() int {
    return me.iID
}

func (me *BadBird) Name() string {
    return me.sName
}

func (me *BadBird) Eat() error {
    fmt.Printf("%v/%v is eating\n", me.Name(), me.ID())
    return nil
}

func (me *BadBird) Fly() error {
    fmt.Printf("%v/%v is flying\n", me.Name(), me.ID())
    return nil
}

func (me *BadBird) Swim() error {
    return errors.New(fmt.Sprintf("%v/%v cannot swimming", me.Name(), me.ID()))
}

Bad ISP dog-instance

BadDog实现IBadAnimal接口.
本来BadDog是不支持Fly()方法的, 但由于接口要求, 因此只能返回无意义错误.

package interface_segregation

import (
    "errors"
    "fmt"
)

type BadDog struct {
    iID int
    sName string
}


func NewBadDog(id int, name string) IBadAnimal {
    return &BadDog{
        iID: id,
        sName: name,
    }
}

func (me *BadDog) ID() int {
    return me.iID
}

func (me *BadDog) Name() string {
    return me.sName
}

func (me *BadDog) Eat() error {
    fmt.Printf("%v/%v is eating\n", me.Name(), me.ID())
    return nil
}

func (me *BadDog) Fly() error {
    return errors.New(fmt.Sprintf("%v/%v cannot fly", me.Name(), me.ID()))
}

func (me *BadDog) Swim() error {
    fmt.Printf("%v/%v is swimming\n", me.Name(), me.ID())
    return nil
}

Good ISP

更好的接口设计. 将动物接口拆分为基本信息接口IGoodAnimal, 以及三个可选的能力接口: ISupportEat, ISupportFly, ISupportSwim

package interface_segregation


type IGoodAnimal interface {
    ID() int
    Name() string
}

type ISupportEat interface {
    Eat() error
}

type ISupportFly interface {
    Fly() error
}

type ISupportSwim interface {
    Swim() error
}

Good ISP good-animal instance

实现IGoodAnimal接口, 提供动物的id,name等基本属性

package interface_segregation

type GoodAnimalInfo struct {
    iID int
    sName string
}


func (me *GoodAnimalInfo) ID() int {
    return me.iID
}

func (me *GoodAnimalInfo) Name() string {
    return me.sName
}

Good ISP good-bird instance

更好的Bird实现, 异味代码更少.
通过集成GoodAnimalInfo实现IGoodAnimal接口, 并选择性实现ISupportEat, ISupportFly.

package interface_segregation

import "fmt"

type GoodBird struct {
    GoodAnimalInfo
}

func NewGoodBird(id int, name string) IGoodAnimal {
    return &GoodBird{
        GoodAnimalInfo{
            id,
            name,
        },
    }
}

func (me *GoodBird) Eat() error {
    fmt.Printf("%v/%v is eating\n", me.Name(), me.ID())
    return nil
}

func (me *GoodBird) Fly() error {
    fmt.Printf("%v/%v is flying\n", me.Name(), me.ID())
    return nil
}

Good ISP good-dog instance

更好的Dog实现, 异味代码更少.
通过集成GoodAnimalInfo实现IGoodAnimal接口, 并选择性实现ISupportEat, ISupportSwim.

package interface_segregation

import "fmt"

type GoodDog struct {
    GoodAnimalInfo
}

func NewGoodDog(id int, name string) IGoodAnimal {
    return &GoodDog{
        GoodAnimalInfo{
            id,
            name,
        },
    }
}

func (me *GoodDog) Eat() error {
    fmt.Printf("%v/%v is eating\n", me.Name(), me.ID())
    return nil
}

func (me *GoodDog) Swim() error {
    fmt.Printf("%v/%v is swimming\n", me.Name(), me.ID())
    return nil
}

Good ISP test

package main

import (
    isp "learning/gooop/principles/interface_segregation"
    "testing"
)

func Test_ISP(t *testing.T) {
    fnLogIfError := func(fn func() error) {
        e := fn()
        if e != nil {
            t.Logf("error = %s\n", e.Error())
        }
    }

    fnTestBadAnimal := func (a isp.IBadAnimal) {
        fnLogIfError(a.Eat)
        fnLogIfError(a.Fly)
        fnLogIfError(a.Swim)
    }

    fnTestBadAnimal(isp.NewBadBird(1, "BadBird"))
    fnTestBadAnimal(isp.NewBadDog(2, "BadDog"))


    fnTestGoodAnimal := func(a isp.IGoodAnimal) {
        if it,ok := a.(isp.ISupportEat);ok {
            fnLogIfError(it.Eat)
        } else {
            t.Logf("%v/%v cannot eat", a.Name(), a.ID())
        }

        if it,ok := a.(isp.ISupportFly);ok {
            fnLogIfError(it.Fly)
        } else {
            t.Logf("%v/%v cannot fly", a.Name(), a.ID())
        }

        if it,ok := a.(isp.ISupportSwim);ok {
            fnLogIfError(it.Swim)
        } else {
            t.Logf("%v/%v cannot swim", a.Name(), a.ID())
        }
    }

    fnTestGoodAnimal(isp.NewGoodBird(11, "GoodBird"))
    fnTestGoodAnimal(isp.NewGoodDog(12, "GoodDog"))
}
$ go test -v interface_segregation_test.go 
=== RUN   Test_ISP
BadBird/1 is eating
BadBird/1 is flying
    interface_segregation_test.go:12: error = BadBird/1 cannot swimming
BadDog/2 is eating
    interface_segregation_test.go:12: error = BadDog/2 cannot fly
BadDog/2 is swimming
GoodBird/11 is eating
GoodBird/11 is flying
    interface_segregation_test.go:42: GoodBird/11 cannot swim
GoodDog/12 is eating
    interface_segregation_test.go:36: GoodDog/12 cannot fly
GoodDog/12 is swimming
--- PASS: Test_ISP (0.00s)
PASS
ok      command-line-arguments  0.002s

References

  1. golang设计模式系列(六)面向对象设计原则-接口隔离原则
  2. 手撸golang 架构设计原则 接口隔离原则