設(shè)計(jì)原則之接口隔離原則

本文是極客時(shí)間里王爭(zhēng)專欄《設(shè)計(jì)模式之美》的學(xué)習(xí)筆記藕坯,你可以通過(guò)鏈接閱讀原文獲取更加詳盡的描述岳服,也可以通過(guò)該鏈接進(jìn)行訂閱和購(gòu)買獲取優(yōu)惠净嘀。

接口隔離原則(ISP)

今天來(lái)看看SOLID中的I, 接口隔離原則隙畜。

如何理解“接口隔離原則”起意?

接口隔離原則(Interface Segregation Principle)吮播,縮寫為ISP变屁。其定義:

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

客戶端不應(yīng)該被強(qiáng)迫依賴它不需要的接口意狠。其中的“客戶端”粟关,可以理解為接口的調(diào)用者或者使用者。

"接口"這個(gè)名詞环戈,在軟件開(kāi)發(fā)中闷板,我們既可以把它看做一組抽象的約定,也可以具體指系統(tǒng)與系統(tǒng)之間的API接口谷市,還可以特指面向?qū)ο缶幊陶Z(yǔ)言中的接口等蛔垢。

理解接口隔離原則的關(guān)鍵,就是理解其中的“接口”二字迫悠。在這條原則中鹏漆,我們可以把“接口”理解以下三種:

  • 一組API接口集合
  • 單個(gè)API接口或函數(shù)
  • OOP中的接口概念

接下來(lái)看看,按照這三種理解方式创泄,在不同的場(chǎng)景下艺玲,這條原則具體是如何解讀和應(yīng)用的。

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

舉個(gè)例子鞠抑》咕郏客戶端開(kāi)發(fā)中,聲明了一組API來(lái)規(guī)范列表類業(yè)務(wù)開(kāi)發(fā)的邏輯搁拙,比如翻頁(yè)秒梳、UITableViewDataSource協(xié)議中的計(jì)算邏輯。

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 {
    
}

假如我們?nèi)缟隙x協(xié)議箕速,有一個(gè)問(wèn)題就是酪碘,業(yè)務(wù)是一個(gè)列表類型的展示,但是沒(méi)有翻頁(yè)的業(yè)務(wù)場(chǎng)景盐茎,但是我遵循了該協(xié)議就必須聲明翻頁(yè)邏輯相關(guān)的字段兴垦。或許可以通過(guò)給TableViewModel中的翻頁(yè)邏輯字段定義默認(rèn)實(shí)現(xiàn),如下所示:

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

但是探越,按照接口隔離原則狡赐,調(diào)用者不應(yīng)該依賴它不需要的接口,沒(méi)有翻頁(yè)邏輯的業(yè)務(wù)钦幔,就不應(yīng)該遵循上述翻頁(yè)的接口枕屉。

將翻頁(yè)的接口單獨(dú)放到另外一個(gè)接口Pageable中,然后將TableViewModel & Pageable打包給具有翻頁(yè)邏輯的列表使用鲤氢,不具有翻頁(yè)邏輯的列表只依賴TableViewModel即可搀庶。

/// 使用`TableView`實(shí)現(xiàn)的列表相關(guān)接口
protocol TableViewModel {
    func numberOfSections() -> Int
    func numberOfRowsIn(section: Int) -> Int
    // ...其他行為約定...
}

/// 翻頁(yè)相關(guān)接口
protocol Pageable {
    var pageSize: Int { get set }
    var pageNum: Int { get set }
    var hasNextPage: Bool { get set }
}

/// 具有翻頁(yè)的列表
typealias PageableTableViewModel = TableViewModel & Pageable

class XXViewModel: PageableTableViewModel {
    
}

另外,Pageable協(xié)議獨(dú)立后铜异,可以與項(xiàng)目中UICollectionView實(shí)現(xiàn)的列表打包結(jié)合使用。

在上面的例子中秸架,我們把接口隔離原則中的接口揍庄,理解為一組接口集合,它可以是某個(gè)視圖的接口东抹,也可以是某個(gè)類庫(kù)的接口等等蚂子。在設(shè)計(jì)視圖或者類庫(kù)接口的時(shí)候,如果部分接口只被部分調(diào)用者使用缭黔,那我們就需要將這部分接口隔離出來(lái)食茎,單獨(dú)給對(duì)應(yīng)的調(diào)用者使用,而不是強(qiáng)迫其他調(diào)用者也依賴這部分不會(huì)被用到的接口馏谨。

把“接口”理解為單個(gè)API接口或函數(shù)

我們?cè)贀Q一種理解方式别渔,把接口理解為單個(gè)接口或函數(shù)(以下簡(jiǎn)稱為“函數(shù)”)。那接口隔離原則就可以理解為:函數(shù)的設(shè)計(jì)要功能單一惧互,不要將多個(gè)不同的功能邏輯在一個(gè)函數(shù)中實(shí)現(xiàn)哎媚。接下來(lái),我們還是通過(guò)一個(gè)例子來(lái)解釋一下喊儡。


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();
  //...省略計(jì)算邏輯...
  return statistics;
}

在上面的代碼中拨与,count()函數(shù)的功能包含很多不同的統(tǒng)計(jì)功能,比如艾猜,求最大值买喧、最小值、平均值等等匆赃。

如果在項(xiàng)目中淤毛,對(duì)每個(gè)統(tǒng)計(jì)需求,Statistics定義的那幾個(gè)統(tǒng)計(jì)信息都有涉及炸庞,那 count() 函數(shù)的設(shè)計(jì)就是合理的钱床。相反,如果每個(gè)統(tǒng)計(jì)需求只涉及Statistics羅列的統(tǒng)計(jì)信息中一部分埠居,比如查牌,有的只需要用到 max事期、minaverage這三類統(tǒng)計(jì)信息纸颜,有的只需要用到 average兽泣、sum。而 count() 函數(shù)每次都會(huì)把所有的統(tǒng)計(jì)信息計(jì)算一遍胁孙,就會(huì)做很多無(wú)用功唠倦,勢(shì)必影響代碼的性能,特別是在需要統(tǒng)計(jì)的數(shù)據(jù)量很大的時(shí)候涮较。所以稠鼻,在這個(gè)應(yīng)用場(chǎng)景下,count() 函數(shù)的設(shè)計(jì)就有點(diǎn)不合理了狂票,我們應(yīng)該按照接口隔離原則候齿,把 count() 函數(shù)拆成幾個(gè)更小粒度的函數(shù),每個(gè)函數(shù)負(fù)責(zé)一個(gè)獨(dú)立的統(tǒng)計(jì)功能闺属。拆分之后的代碼如下所示:


public Long max(Collection<Long> dataSet) { //... }
public Long min(Collection<Long> dataSet) { //... } 
public Long average(Colletion<Long> dataSet) { //... }
// ...省略其他統(tǒng)計(jì)函數(shù)...

接口隔離原則跟單一職責(zé)原則有點(diǎn)類似慌盯,不過(guò)稍微還是有點(diǎn)區(qū)別。

  • 單一職責(zé)原則針對(duì)的是模塊掂器、類亚皂、接口的設(shè)計(jì)。而接口隔離原則相對(duì)于單一職責(zé)原則国瓮,它更側(cè)重于接口的設(shè)計(jì);
  • 接口隔離原則的思考的角度不同灭必。它提供了一種判斷接口是否職責(zé)單一的標(biāo)準(zhǔn):通過(guò)調(diào)用者如何使用接口來(lái)間接地判定。如果調(diào)用者只使用部分接口或接口的部分功能巍膘,那接口的設(shè)計(jì)就不夠職責(zé)單一厂财。

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

我們還可以把“接口”理解為 OOP 中的接口概念,比如 iOS 中的協(xié)議(Protocol)峡懈,這里不考慮利用協(xié)議實(shí)現(xiàn)委托的場(chǎng)景璃饱。舉一個(gè)簡(jiǎn)單的例子。

假如項(xiàng)目中要做習(xí)題的功能肪康,分為兩種模式:練習(xí)模式和挑戰(zhàn)模式荚恶。練習(xí)模式的習(xí)題是客戶端隨機(jī)生成,挑戰(zhàn)模式下的習(xí)題是從數(shù)據(jù)庫(kù)中獲取×字В現(xiàn)定義有如下接口:

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

class ChallengeService: LearnService {
    // ...忽略實(shí)現(xiàn)...
}

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

現(xiàn)增加錯(cuò)題本谒撼,在練習(xí)模式下,錯(cuò)誤習(xí)題記錄到錯(cuò)題本雾狈,而在挑戰(zhàn)模式下廓潜,無(wú)需記錄。這種情況下,新增接口

func record(wrong: Equation?)

是應(yīng)該放置在LearnService中還是另新增協(xié)議RecordService單獨(dú)維護(hù)呢辩蛋,如下:

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

根據(jù)接口隔離原則呻畸,應(yīng)該使用新增RecordService協(xié)議單獨(dú)維護(hù),這樣可以避免在挑戰(zhàn)模式下依賴不需要的接口悼院。雖然伤为,在iOS中可以將接口定義成可選類型(optional),來(lái)避免實(shí)現(xiàn)不需要的接口据途,但是這樣的話绞愚,違背了單一職責(zé)原則和接口隔離原則。

對(duì)于第三方庫(kù)Reusable中颖医,開(kāi)發(fā)者也是將NibLoadable協(xié)議和Reusable協(xié)議獨(dú)立位衩,如下:

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

滿足接口隔離原則,避免實(shí)現(xiàn)者依賴不需要的接口熔萧。

重點(diǎn)回顧

  1. 如何理解“接口隔離原則”蚂四?

理解“接口隔離原則”的重點(diǎn)是理解其中的“接口”二字。這里有三種不同的理解哪痰。

如果把“接口”理解為一組接口集合,可以是某個(gè)微服務(wù)的接口久妆,也可以是某個(gè)類庫(kù)的接口等晌杰。如果部分接口只被部分調(diào)用者使用,我們就需要將這部分接口隔離出來(lái)筷弦,單獨(dú)給這部分調(diào)用者使用肋演,而不強(qiáng)迫其他調(diào)用者也依賴這部分不會(huì)被用到的接口。

如果把“接口”理解為單個(gè) API 接口或函數(shù)烂琴,部分調(diào)用者只需要函數(shù)中的部分功能爹殊,那我們就需要把函數(shù)拆分成粒度更細(xì)的多個(gè)函數(shù),讓調(diào)用者只依賴它需要的那個(gè)細(xì)粒度函數(shù)奸绷。

如果把“接口”理解為 OOP 中的接口梗夸,也可以理解為面向?qū)ο缶幊陶Z(yǔ)言中的接口語(yǔ)法。那接口的設(shè)計(jì)要盡量單一号醉,不要讓接口的實(shí)現(xiàn)類和調(diào)用者反症,依賴不需要的接口函數(shù)。

  1. 接口隔離原則與單一職責(zé)原則的區(qū)別

單一職責(zé)原則針對(duì)的是模塊畔派、類铅碍、接口的設(shè)計(jì)。接口隔離原則相對(duì)于單一職責(zé)原則线椰,一方面更側(cè)重于接口的設(shè)計(jì)胞谈,另一方面它的思考角度也是不同的。接口隔離原則提供了一種判斷接口的職責(zé)是否單一的標(biāo)準(zhǔn):通過(guò)調(diào)用者如何使用接口來(lái)間接地判定。如果調(diào)用者只使用部分接口或接口的部分功能烦绳,那接口的設(shè)計(jì)就不夠職責(zé)單一卿捎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市爵嗅,隨后出現(xiàn)的幾起案子娇澎,更是在濱河造成了極大的恐慌,老刑警劉巖睹晒,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件趟庄,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡伪很,警方通過(guò)查閱死者的電腦和手機(jī)戚啥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)锉试,“玉大人猫十,你說(shuō)我怎么就攤上這事〈舾牵” “怎么了拖云?”我有些...
    開(kāi)封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)应又。 經(jīng)常有香客問(wèn)我宙项,道長(zhǎng),這世上最難降的妖魔是什么株扛? 我笑而不...
    開(kāi)封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任尤筐,我火速辦了婚禮,結(jié)果婚禮上洞就,老公的妹妹穿的比我還像新娘盆繁。我一直安慰自己,他們只是感情好旬蟋,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布油昂。 她就那樣靜靜地躺著,像睡著了一般倾贰。 火紅的嫁衣襯著肌膚如雪秕狰。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天躁染,我揣著相機(jī)與錄音鸣哀,去河邊找鬼。 笑死吞彤,一個(gè)胖子當(dāng)著我的面吹牛我衬,可吹牛的內(nèi)容都是我干的叹放。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼挠羔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼井仰!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起破加,我...
    開(kāi)封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤俱恶,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后范舀,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體合是,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年锭环,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了聪全。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辅辩,死狀恐怖难礼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情玫锋,我是刑警寧澤蛾茉,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站撩鹿,受9級(jí)特大地震影響臀稚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜三痰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望窜管。 院中可真熱鬧散劫,春花似錦、人聲如沸幕帆。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)失乾。三九已至常熙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間碱茁,已是汗流浹背裸卫。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纽竣,地道東北人墓贿。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓茧泪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親聋袋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子队伟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容