设计原则之接口隔离原则

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

接口隔离原则(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<Long> dataSet) {
  Statistics statistics = new Statistics();
  //...省略计算逻辑...
  return statistics;
}

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

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


public Long max(Collection<Long> dataSet) { //... }
public Long min(Collection<Long> dataSet) { //... } 
public Long average(Colletion<Long> 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. 接口隔离原则与单一职责原则的区别

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容