引用wikipedia上对接口隔离原则的解释:
接口隔离原则(英语:interface-segregation principles, 缩写:ISP)指明客户(client)不应被迫使用对其而言无用的方法或功能。接口隔离原则(ISP)拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。这种缩小的接口也被称为角色接口(role interfaces)。接口隔离原则(ISP)的目的是系统解开耦合,从而容易重构,更改和重新部署。接口隔离原则是在SOLID中五个面向对象设计(OOD)的原则之一,类似于在GRASP中的高内聚性。
保持小的接口,这样用户就不会最终依赖他们不需要的东西了。 Bob大叔创造了这个原则,关于它的更多细节你可以在他的博客上找到。这个原则清楚地定义了它的要求,可能是所有其他SOLID原则中最好的一个。 我们不应该把它简单地理解为仅仅是一个方法接口,而应该更多地从接口所拥有的功能内聚的角度来看待它。让我们来看看下面的代码。
type User interface {
AddToShoppingCart(product Product)
IsLoggedIn() bool
Pay(money Money) error
HasPremium() bool
HasDiscountFor(product Product) bool
//
// some additional methods
//
}
type Guest struct {
cart ShoppingCart
//
// some additional fields
//
}
func (g *Guest) AddToShoppingCart(product Product) {
g.cart.Add(product)
}
func (g *Guest) IsLoggedIn() bool {
return false
}
func (g *Guest) Pay(Money) error {
return errors.New("user is not logged in")
}
func (g *Guest) HasPremium() bool {
return false
}
func (g *Guest) HasDiscountFor(Product) bool {
return false
}
type NormalCustomer struct {
cart ShoppingCart
wallet Wallet
//
// some additional fields
//
}
func (c *NormalCustomer) AddToShoppingCart(product Product) {
c.cart.Add(product)
}
func (c *NormalCustomer) IsLoggedIn() bool {
return true
}
func (c *NormalCustomer) Pay(money Money) error {
return c.wallet.Deduct(money)
}
func (c *NormalCustomer) HasPremium() bool {
return false
}
func (c *NormalCustomer) HasDiscountFor(Product) bool {
return false
}
type PremiumCustomer struct {
cart ShoppingCart
wallet Wallet
policies []DiscountPolicy
//
// some additional fields
//
}
func (c *PremiumCustomer) AddToShoppingCart(product Product) {
c.cart.Add(product)
}
func (c *PremiumCustomer) IsLoggedIn() bool {
return true
}
func (c *PremiumCustomer) Pay(money Money) error {
return c.wallet.Deduct(money)
}
func (c *PremiumCustomer) HasPremium() bool {
return true
}
func (c *PremiumCustomer) HasDiscountFor(product Product) bool {
for _, p := range c.policies {
if p.IsApplicableFor(c, product) {
return true
}
}
return false
}
type UserService struct {
//
// some fields
//
}
func (u *UserService) Checkout(ctx context.Context, user User, product Product) error {
if !user.IsLoggedIn() {
return errors.New("user is not logged in")
}
var money Money
//
// some calculation
//
if user.HasDiscountFor(product) {
//
// apply discount
//
}
return user.Pay(money)
}
让我们假设我们想提供一个购物的应用程序。其中一种方法是定义一个接口User
,就像我们在代码例子中做的那样。这个界面拥有一个用户可以拥有的许多功能。
在我们的平台上,一个用户可以在ShoppingCart上添加一个产品。他们可以购买它。他们可以获得某个产品的折扣。唯一的问题是,只有一个特定的用户可以做所有这些事情。
这个接口的实际实现是三个结构。第一个是Guest结构。它应该是一个没有登录到我们系统的用户,但至少他们可以向ShoppingCart添加产品。
第二个实现是NormalCustomer。它可以做任何Guest可以做的事情,另外还可以购买产品。
第三个实现是PremiumCustomer,它可以使用我们系统的所有功能。
现在,请看所有这三个结构。只有PremiumCustomer需要所有三个方法。也许我们可以把所有这些方法都分配给NormalCustomer,但可以肯定的是,Guest需要的方法不超过两个。
HasPremium和HasDiscountFor方法对Guest没有任何意义。如果该结构代表未登录的用户,我们为什么还要考虑实现折扣方法?
在这里,我们甚至可以调用带有“method is not implemented”的panic方法。在通常情况下,我们甚至不应该从Guest调用HasPremium方法。
我们所做的这一切是为了在UserService内部增加通用性,以便在同一个地方用相同的代码处理所有类型的用户。但是,正因为如此,我们需要实现一堆不用的方法。
所以,为了有更好的通用性,导致:
- 许多struct有未使用的方法。
- 需要以某种方式标记方法,以便其他人不使用这些方法。
- 额外的单元测试代码。
- 奇怪的多态性。
- …
下面,我们遵循接口隔离原则重构下以上代码。
如何遵守接口隔离
围绕最小的内聚特性组构建接口。
我们不需要在这里发明一些空间科学。唯一需要的是定义一个提供完整功能集的最小接口。让我们检查下面的代码:
type User interface {
AddToShoppingCart(product Product)
//
// some additional methods
//
}
type LoggedInUser interface {
User
Pay(money Money) error
//
// some additional methods
//
}
type PremiumUser interface {
LoggedInUser
HasDiscountFor(product Product) bool
//
// some additional methods
//
}
type Guest struct {
cart ShoppingCart
//
// some additional fields
//
}
func (g *Guest) AddToShoppingCart(product Product) {
g.cart.Add(product)
}
type NormalCustomer struct {
cart ShoppingCart
wallet Wallet
//
// some additional fields
//
}
func (c *NormalCustomer) AddToShoppingCart(product Product) {
c.cart.Add(product)
}
func (c *NormalCustomer) Pay(money Money) error {
return c.wallet.Deduct(money)
}
type PremiumCustomer struct {
cart ShoppingCart
wallet Wallet
policies []DiscountPolicy
//
// some additional fields
//
}
func (c *PremiumCustomer) AddToShoppingCart(product Product) {
c.cart.Add(product)
}
func (c *PremiumCustomer) Pay(money Money) error {
return c.wallet.Deduct(money)
}
func (c *PremiumCustomer) HasDiscountFor(product Product) bool {
for _, p := range c.policies {
if p.IsApplicableFor(c, product) {
return true
}
}
return false
}
type UserService struct {
//
// some fields
//
}
func (u *UserService) Checkout(ctx context.Context, user User, product Product) error {
loggedIn, ok := user.(LoggedInUser)
if !ok {
return errors.New("user is not logged in")
}
var money Money
//
// some calculation
//
if premium, ok := loggedIn.(PremiumUser); ok && premium.HasDiscountFor(product) {
//
// apply discount
//
}
return loggedIn.Pay(money)
}
现在,我们有三个接口,而不是一个。PremiumUser
嵌入LoggedInUser
,后者嵌入User
。此外,它们中的每一个都引入了一种方法。
User
现在仅代表尚未在我们的平台上进行身份验证的客户。对于此类类型,我们知道他们可以使用购物车的功能。
新的LoggedInUser
接口代表所有经过身份验证的客户,而PremiumUser
接口代表所有具有付费高级帐户的经过身份验证的客户。
请注意这一点:我们确实添加了另外两个接口,但我们删除了两个方法:IsLoggedIn和HasPremium。这些方法不是我们接口签名的一部分。但是,没有它们,我们怎么能工作呢?
正如您在UserService中看到的那样,我们没有使用bool返回值方法,而是阐明了用户接口的子类型。如果User
实现了 LoggedInUser
,我们就知道该客户是经过身份验证的。
此外,如果用户实现PremiumUser
,我们知道我们谈论的是具有高级帐户的客户。因此,通过转换,我们已经检查了一些业务规则。
除了这两种方法之外,之前的所有struct现在都更加轻量级。他们每个人都有五种方法,其中许多方法根本没有使用,现在他们只有他们真正需要的方法。
其他例子
尽管提供小而灵活的接口总是好的,但我们应该考虑到它们的目的来介绍它们。添加小接口以使它们更简单,但仍然在同一结构中将它们一起实现并没有太多意义。
让我们检查下面的例子:
// too much splitting
type UserWithFirstName interface {
FirstName() string
}
type UserWithLastName interface {
LastName() string
}
type UserWithFullName interface {
FullName() string
}
// optimal splitting
type UserWithName interface {
FirstName() string
LastName() string
FullName() string
}
当我们过多地拆分接口时,就是这种情况。是的,我们可以为每个方法提供一个接口,现在将它们定义为角色接口。这样的单方法接口有时很好,但在这里不是。
显然,如果客户在我们的平台上注册,他们需要提供他们的名字和姓氏以用于计费目的。因此,我们的用户将需要"名字"和"姓氏"方法,当然,还有"全名"。
在这种情况下,将这三种方法拆分为三个接口没有意义,因为这三种方法总是在一起。因此,这不是单方法接口的正确示例。
但是,一个很好的例子是:
package io
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
type WriteCloser interface {
Writer
Closer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Go中的完美例子是IO包。它提供了许多用于处理 I/O 操作的代码和接口,并且可能所有 Go 开发人员都至少使用过一次此包。
它提供了Reader
, Writer
, Closer
, Seeker
接口。它们中的每一个只定义一个方法: Read
, Write
, Close
和 Seek
。我们使用这些方法来读取,写入,从特定源搜索和搜索一段字节,并关闭该源。
为了使此类源具有更大的灵活性,所有功能都放置在其接口中。后来,他们一起构建更复杂的接口,如WriteCloser
,ReadWriteCloser
等。
总结
接口隔离原则是第四个SOLID
原则,它代表单词SOLID
中的字母I
。它教会我们尽可能小地定义接口。
每当我们想要涵盖更多类型时,我们应该使用不同的接口来保护它们。我们应该避免将接口做得太小,而是提供完整的功能。
参考:
1、https://levelup.gitconnected.com/practical-solid-in-golang-interface-segregation-principle-f272c2a9a270
2、https://zh.wikipedia.org/wiki/%E6%8E%A5%E5%8F%A3%E9%9A%94%E7%A6%BB%E5%8E%9F%E5%88%99