设计原则之接口隔离原则

本文是极客时间里王争专栏《设计模式之美》的学习笔记,你可以通过链接阅读原文获取更加详尽的描述,也可以通过该链接进行订阅和购买获取优惠。

接口隔离原则(ISP)

今天来看看SOLID中的I, 接口隔离原则。

如何理解“接口隔离原则”?

接口隔离原则(Interface Segregation Principle),缩写为ISP。其定义:

Clients should not be forced to depend upon interfaces that they do not use。

客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。

"接口"这个名词,在软件开发中,我们既可以把它看做一组抽象的约定,也可以具体指系统与系统之间的API接口,还可以特指面向对象编程语言中的接口等。

理解接口隔离原则的关键,就是理解其中的“接口”二字。在这条原则中,我们可以把“接口”理解以下三种:

  • 一组API接口集合
  • 单个API接口或函数
  • OOP中的接口概念

接下来看看,按照这三种理解方式,在不同的场景下,这条原则具体是如何解读和应用的。

把“接口”理解成一组API接口集合

举个例子。客户端开发中,声明了一组API来规范列表类业务开发的逻辑,比如翻页、UITableViewDataSource协议中的计算逻辑。

protocol TableViewModel {
    var pageSize: Int { get set }
    var pageNum: Int { get set }
    var hasNextPage: Bool { get set }
    func numberOfSections() -> Int
    func numberOfRowsIn(section: Int) -> Int
    // ...其他行为约定...
}

class XXViewModel: TableViewModel {
    
}

假如我们如上定义协议,有一个问题就是,业务是一个列表类型的展示,但是没有翻页的业务场景,但是我遵循了该协议就必须声明翻页逻辑相关的字段。或许可以通过给TableViewModel中的翻页逻辑字段定义默认实现,如下所示:

extension TableViewModel {
    var pageSize: Int {
        get { return 0 }
        set {}
    }
    
    var pageNum: Int {
        get { return 1 }
        set {}
    }
    
    var hasNextPage: Bool {
        get { return false }
        set {}
    }
}

但是,按照接口隔离原则,调用者不应该依赖它不需要的接口,没有翻页逻辑的业务,就不应该遵循上述翻页的接口。

将翻页的接口单独放到另外一个接口Pageable中,然后将TableViewModel & Pageable打包给具有翻页逻辑的列表使用,不具有翻页逻辑的列表只依赖TableViewModel即可。

/// 使用`TableView`实现的列表相关接口
protocol TableViewModel {
    func numberOfSections() -> Int
    func numberOfRowsIn(section: Int) -> Int
    // ...其他行为约定...
}

/// 翻页相关接口
protocol Pageable {
    var pageSize: Int { get set }
    var pageNum: Int { get set }
    var hasNextPage: Bool { get set }
}

/// 具有翻页的列表
typealias PageableTableViewModel = TableViewModel & Pageable

class XXViewModel: PageableTableViewModel {
    
}

另外,Pageable协议独立后,可以与项目中UICollectionView实现的列表打包结合使用。

在上面的例子中,我们把接口隔离原则中的接口,理解为一组接口集合,它可以是某个视图的接口,也可以是某个类库的接口等等。在设计视图或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。

把“接口”理解为单个API接口或函数

我们再换一种理解方式,把接口理解为单个接口或函数(以下简称为“函数”)。那接口隔离原则就可以理解为:函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。接下来,我们还是通过一个例子来解释一下。


public class Statistics {
  private Long max;
  private Long min;
  private Long average;
  private Long sum;
  private Long percentile99;
  private Long percentile999;
  //...省略constructor/getter/setter等方法...
}

public Statistics count(Collection dataSet) {
  Statistics statistics = new Statistics();
  //...省略计算逻辑...
  return statistics;
}

在上面的代码中,count()函数的功能包含很多不同的统计功能,比如,求最大值、最小值、平均值等等。

如果在项目中,对每个统计需求,Statistics定义的那几个统计信息都有涉及,那 count() 函数的设计就是合理的。相反,如果每个统计需求只涉及Statistics罗列的统计信息中一部分,比如,有的只需要用到 maxminaverage这三类统计信息,有的只需要用到 averagesum。而 count() 函数每次都会把所有的统计信息计算一遍,就会做很多无用功,势必影响代码的性能,特别是在需要统计的数据量很大的时候。所以,在这个应用场景下,count() 函数的设计就有点不合理了,我们应该按照接口隔离原则,把 count() 函数拆成几个更小粒度的函数,每个函数负责一个独立的统计功能。拆分之后的代码如下所示:


public Long max(Collection dataSet) { //... }
public Long min(Collection dataSet) { //... } 
public Long average(Colletion dataSet) { //... }
// ...省略其他统计函数...

接口隔离原则跟单一职责原则有点类似,不过稍微还是有点区别。

  • 单一职责原则针对的是模块、类、接口的设计。而接口隔离原则相对于单一职责原则,它更侧重于接口的设计;
  • 接口隔离原则的思考的角度不同。它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

把“接口”理解为 OOP 中的接口概念

我们还可以把“接口”理解为 OOP 中的接口概念,比如 iOS 中的协议(Protocol),这里不考虑利用协议实现委托的场景。举一个简单的例子。

假如项目中要做习题的功能,分为两种模式:练习模式和挑战模式。练习模式的习题是客户端随机生成,挑战模式下的习题是从数据库中获取。现定义有如下接口:

protocol LearnService: AnyObject {
    func fetchSectionItems(isInit: Bool) -> [Equation]
    func currentItem() -> Equation?
    func hasFinishSection() -> Bool
    //...其他接口...
}

class ChallengeService: LearnService {
    // ...忽略实现...
}

// LearnService的使用
class ExerciseViewController: UIViewController {
    var service: LearnService!
    // ...省略其他属性...
  
    func fetchDataAndRefresh(isInit: Bool = false) {
        let items = service.fetchSectionItems(isInit: isInit)
        guard !items.isEmpty else {
            return
        }
                // ...其他逻辑代码...
    }
}

现增加错题本,在练习模式下,错误习题记录到错题本,而在挑战模式下,无需记录。这种情况下,新增接口

func record(wrong: Equation?)

是应该放置在LearnService中还是另新增协议RecordService单独维护呢,如下:

protocol RecordService: AnyObject {
        func record(wrong: Equation?)
}

根据接口隔离原则,应该使用新增RecordService协议单独维护,这样可以避免在挑战模式下依赖不需要的接口。虽然,在iOS中可以将接口定义成可选类型(optional),来避免实现不需要的接口,但是这样的话,违背了单一职责原则和接口隔离原则。

对于第三方库Reusable中,开发者也是将NibLoadable协议和Reusable协议独立,如下:

public protocol Reusable: class {
  /// The reuse identifier to use when registering and later dequeuing a reusable cell
  static var reuseIdentifier: String { get }
}

public protocol NibLoadable: class {
  /// The nib file to use to load a new instance of the View designed in a XIB
  static var nib: UINib { get }
}

public typealias NibReusable = Reusable & NibLoadable

满足接口隔离原则,避免实现者依赖不需要的接口。

重点回顾

  1. 如何理解“接口隔离原则”?

理解“接口隔离原则”的重点是理解其中的“接口”二字。这里有三种不同的理解。

如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。

如果把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。

如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

  1. 接口隔离原则与单一职责原则的区别

单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

你可能感兴趣的:(设计原则之接口隔离原则)