接口隔离原则的理解
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 个优点。
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
- 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
- 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
- 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
在具体应用接口隔离原则时,应该根据以下几个规则来衡量。
- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
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