本文是極客時(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è)秒梳、UITableView
的DataSource
協(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
事期、min
、average
這三類統(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)回顧
- 如何理解“接口隔離原則”蚂四?
理解“接口隔離原則”的重點(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ù)。
- 接口隔離原則與單一職責(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é)單一卿捎。