[5+1]接口隔離原則(二)

前言java

面向對象的SOLID設計原則,外加一個迪米特法則,就是咱們常說的5+1設計原則。圖片↑ 五個,再加一個,就是5+1個。哈哈哈。
這六個設計原則的位置有點不上不下。論原則性和理論指導意義,它們不如封裝繼承抽象或者高內聚低耦合,因此在寫代碼或者code review的時候,它們很難成爲「應該這樣作」或者「不該該這樣作」的一個有說服力的理由。論靈活性和實踐操做指南,它們又不如設計模式或者架構模式,因此即便你能說出來某段代碼違反了某項原則,經常也很難明確指出錯在哪兒、要怎麼改。
因此,這裏來討論討論這六條設計原則的「爲何」和「怎麼作」。順帶,做爲面向對象設計思想的一環,這裏也想聊聊它們與抽象、高內聚低耦合、封裝繼承多態之間的關係。
數據庫

[5+1] 接口隔離原則(一)設計模式

上一部分在這裏
架構




[5+1] 接口隔離原則(二)app


接口隔離與面向對象
我記得,項目管理中有一項「干係人管理」。在干係人管理中,咱們須要識別出與項目存在利益關係的各方,而後肯定各自的關注點,最後根據不一樣的關注點作不一樣的溝通協做、資源協調、指望管理、結果與過程彙報等。圖片項目干係人管理
在干係人管理中,咱們須要注意一點:不一樣關係人的關注點大多不同。用戶關注能不能知足需求;客戶關注能不能賺到錢;boss大佬關注結果,項目經理關注過程;產品經理關注功能,技術經理關注質量;對接系統的開發關注接口文檔,系統內部開發關注流程、類和庫表設計……
在實踐中,咱們經常會從一套基礎數據中提取不一樣內容,以知足不一樣干係人的不一樣關注點。例如,一份詳細設計文檔就能夠知足產品經理、技術經理、對接開發和內部開發的關注點;一份分工排期表既可讓大佬知道何時有結果,也可讓項目經理知道過程當中須要注意哪些人、把控哪些點。
儘管有不少不一樣數據都來自同一個源頭,但咱們通常不會把基礎數據直接分發給不一樣的干係人。項目經理把進度日報發給boss,boss也許眉頭一皺嫌他太囉嗦而後把他開掉了。遊戲策劃把發給客戶的抽卡/氪金分析數據捅給用戶,用戶也許眉頭一皺遊戲太垃圾而後就退遊保肝了。圖片說難聽點就是「見人說人話見鬼說鬼話」
面向對象中也有相似的設計思路。有時候,儘管底層使用的是同一個類,可是,面向不一樣調用方時,咱們會提供不一樣的接口。典型的例子就是LinkedList:分佈式

public class LinkedList<E>    extends AbstractSequentialList<E>    implements List<E>, Deque<E>, Cloneable, java.io.Serializable{    // 略}public interface Deque<E> extends Queue<E> {    // 略}

LinkedList類實現了List<E>(從而實現了Collection<E>)、Deque<E>(從而實現了Queue<E>)等接口。於是,當須要使用有序集合、而且隨機寫入數據時,咱們就能夠經過List<E>接口來操做它。若是隻須要從表頭寫入、從表尾讀取時,咱們也能夠只用Queue<E>接口來操做它:
List<String> list = new LinkedList<>();list.add("1");list.add("2");list.add(1,"0");
Queue<String> queue = new LinkedList<>();queue.add("1");String tail = queue.poll();

相似的還有new ConcurrentHashmap().keySet()——明明是ConcurrentHashMap,生被用成了ConcurrentHashSet。

咱們的業務系統中也有這種類,最典型的就是數據庫操做類。通常來講,數據庫操做的增刪改查都會放在同一個Dao或者Mapper類中。其中的讀操做還好說,寫操做必須嚴密封鎖起來,以確保只能在業務操做、業務事務中以一致性方式寫入數據。不然的話,「你也說聊齋,我也說聊齋」,你們亂塗亂畫起來,豈不要把嬰寧嫁給寧採臣了? 圖片 聶小倩第一個不答應
封鎖寫操做的最簡單方式,就是接口隔離:讀操做和寫操做定義成不一樣的接口。讀接口能夠任意使用;寫接口只容許業務操做使用,其它操做想要寫入數據庫,必須調用業務操做接口。這樣,就能夠避免完整業務數據被部分寫入、進而違反業務一致性的問題了。
從不一樣的方面描述同一件事情,不管在管理上仍是在面向對象設計上,都是一種很高效並且很必要的工做方式。在管理上,咱們把這種工做方式叫作「見人說人話,見鬼說鬼話」;在面向對象設計上,咱們把它叫作「接口隔離原則」。


接口隔離與抽象
不少時候,咱們一提到抽象,就會直接把它與接口劃上等號。因此很天然的,談到接口隔離與抽象,咱們也會直接地想到把「接口隔離」與「更小的抽象」劃上等號。
這個觀點倒也沒有什麼大問題。尤爲是當接口隔離原則被簡化爲「把龐大而臃腫的接口拆分紅更小、更具體的接口」時,它與抽象之間的關係天然就只能是「把龐大而臃腫的抽象拆分爲更小、更具體的抽象」了。
例如,有時咱們會在Dao層之上,增長一個DbService層,將其用做數據庫操做的更高層抽象:ide

public interface  DbService<T,Q>{    /**查詢一條數據*/    Optional<T> query(Q query);    /**新增一條數據*/    T save(T data);    /**更新數據*/    Optional<T> edit(T data, Q query);    /**刪除數據*/    int remove(Q query);}

這個數據庫操做抽象看起來不錯,並且蠻通用的。不過,在業務中,咱們能夠容許任一功能模塊都來讀數據,但只能容許在特定的業務流程中寫數據。所以,讀操做和寫操做應當區別對待。

然而,DbService所定義的抽象卻把讀、寫兩個操做同時暴露了出來:只要能夠讀數據,就能夠寫數據。例如:
public class QueryDataService{    private DbService<Data, Query> dbService;
   public Data queryData(Long id){        Query q = new Query(id);        Data data = dbService.query(q)        return data;    }}

實際上這個類中只須要查詢數據。可是注入DbService接口以後,這個類也具有了寫數據的能力。也就是說,「寫操做」被泄露到了限定的業務流程以外。雖然大多數狀況下,泄露出去的「寫操做」都是可控的;然而對「我不想賣、你不能賣」的抽象設計來講,這就是一個設計上的問題。

要改正這個問題,其實也很簡單:把讀寫操做拆分到兩個接口中就能夠了:
public interface DbReader<T,Q>{    /**查詢一條數據*/    Optional<T> query(Q query);}public interface DbWriter<T,Q>{    /**新增一條數據*/    T save(T data);    /**更新數據*/    Optional<T> edit(T data, Q query);    /**刪除數據*/    int remove(Q query);}/**保留DbService,以便:* 1. 兼容老代碼 * 2. 爲某些特定業務提供讀寫雙操做的接口,*/public interface  DbService<T,Q> extends DbReader<T,Q>, DbWriter<T,Q>{}

這種接口拆分,不正是接口隔離原則所要求的嗎?

接口隔離原則與抽象之間的關係,不只僅是這樣的接口拆分。若是說設計抽象的目的是「我不想賣、你不能買」,那麼接口隔離原則的要求就是「我不想買、你不能賣」——對,合起來就是「不能強買強賣」。嚴格的說,同時符合了這兩個要求的抽象,纔是合格的設計。 圖片 旅遊和購物也應該「隔離」開
就像前面列舉的一些例子同樣:調用者並不關心doSth()方法的步驟,接口就不該該提供諸如step1()/step2()這樣的方法;調用者只須要approve()方法,接口就不該該提供queryUser()方法。雖然從抽象設計的角度來看,它們的確是服務方自願提供的方法。但從接口隔離原則的角度來看,這些方法可不是調用方想要的東西。
這就像去理髮店理髮時,Tony老師推銷給你一張五折會員卡並說服你預存一千塊錢同樣——看起來是他讓顧客獲得了實惠,其實是他「綁架」了顧客下一次的消費行爲。對於消費者來講,換一家理髮店的成本也許不高,況且原先的卡還能夠掛鹹魚賣掉;可是對系統來講,一次重構調整的成本可就很差說了。若是能把重構範圍約束在抽象內部,那大概就花個工本費;若是重構範圍包括了接口的全部調用方——尤爲是分佈式環境下的接口調用方——那簡直就是地獄難度了。


接口隔離與高內聚低耦合
其實前面已經把接口隔離與高內聚低耦合之間的關係表述得很清楚了:適當地遵循接口隔離原則,有助於建立高內聚低耦合的抽象和模塊。
例如,把SomeService接口中的step1()/step2()等方法刪掉,只保留doSth()方法,不只遵循了接口隔離原則,也下降了服務調用者與提供者之間的耦合度。結合《細說幾種耦合》來看,這個改造至少能夠避免雙方產生內容耦合。
而把FlowService中關於用戶的功能拆分到UserService中,則能夠有效地提升對應模塊的內聚性:用戶相關功能和流程相關功能都放到各自的模塊中,內聚性至少能夠從偶然內聚提升到過程內聚甚至順序內聚(參考《細說幾種內聚》)。內聚性提升了,天然地,用戶模塊和流程模塊之間的耦合性也下降了。
雖然遵循接口隔離原則有助於提升內聚性、下降耦合性,可是「過猶不及」,過於強調接口隔離,有時反而會下降內聚、增長耦合。
例如,Java中的Iterator接口中,就有這樣兩個方法:ui

public interface Iterator<E> {
   boolean hasNext();
   E next();
   // 其它方法,略}

大多數狀況下,hasNext()方法和next()都是配套使用的。能夠說,這兩個方法在一塊兒,才能構成一個完整的迭代器抽象。若是咱們機械地套用接口隔離原則,把它倆硬生生地拆分到兩個不一樣的接口中,反而下降了這個抽象設計的內聚性。

實際上,前面所討論的把DbService接口拆分紅DbReader和DbWriter的例子,也可能產生相似的問題。若是DbService不是供業務邏輯使用、而是僅僅提供資源服務,那麼增刪改查操做就沒有必要拆開了。 圖片 好好的傢伙事兒拆得稀碎也不行



接口隔離與封裝繼承多態
接口隔離原則與封裝的關係很是容易理解;相比之下,它與繼承、多態之間的關係就不那麼清晰了。
在面向對象中,接口是實現封裝特性的最有力也最多見的手段。與接口密切相關的接口隔離原則,天然也與封裝特性有着密切的關係。
相信咱們不少人都被「過分包裝」噁心過:實際的商品重不到三兩、大不過拳頭,非要左一層「精美包裝」、右一層「豪華包裝」。結果呢?買的人花一筆冤枉錢買了個不痛快,用的人拆一大堆空盒子用得不痛快。哪怕是用來收禮,若是知道這「禮物」的90%是包裝盒,送禮的人恐怕也會以爲臉面無光吧!spa

圖片

過分包裝設計


冗長的接口和過分包裝的問題同樣,都是自覺得是地把一大堆用戶不須要的、深惡痛絕的東西強加給用戶。這種「強加於人」,在市場營銷中叫「捆綁銷售」,在面向對象中就叫「不當封裝」:該「封」起來的沒有作好密封,不應「裝」進來的一股腦地裝到了一塊兒。
可見,恰當的接口隔離能夠保證咱們的類擁有更好的封裝性。一樣的,作好接口隔離,也能在類的繼承方面給咱們提供便利。
在Java中,因爲接口方法都只有方法簽名、沒有方法體,所以,實現類只有兩個選擇:將自身聲明爲抽象類,或者實現接口中的全部方法。雖然Java8容許接口方法定義方法體、以提供一個默認實現,但這個默認實現的功能很是弱,基本只能用來向下兼容,真要實現業務功能,還得靠實現類來重寫方法。總之,咱們仍能夠認爲:接口中聲明的方法,最終都要被實現類重寫。由此能夠推斷:一個只有三個方法的接口,和一個包含了十三個方法的接口相比,顯然是前者對實現類更友好。
固然,咱們也能夠採用接口-基類-實現類的層次結構,來減小實現大接口時的開發量。例以下面這樣:

public interface Service{    void method1();    void method2();    // 中間若干方法,略    void methodN();    }public class BaseService implements Service{    public void method1(){        // 默認實現    }    public void method2(){        // 默認實現    }    // 中間若干方法的默認實現    public void methodN(){        // 默認實現    }}
public class SubService extends BaseService{    public void method5(){        // 實現本身所需方法    }}

這樣作,確實能夠解決每次實現接口都須要重寫全部方法的問題。不過,此時咱們又要面對另外一個可能更嚴重的問題:若是咱們的SubService只須要提供method5()這一個方法,而不須要提供BaseService中的其它功能,也就是說前者與後者並不知足繼承所要求的「is-a」關係,此時咱們讓SubService繼承BaseService,真的不是撿起芝麻丟了西瓜嗎?

誠然,並非全部的大接口都會有這樣的問題;但幾乎全部的小接口都沒有這種問題。反過來講,使用小接口時,咱們幾乎不用擔憂出現過分繼承問題;而使用大接口時,咱們至少應該認真思考一下這個接口及其繼承層次是否合理。這也就是此前提到的接口隔離原則的定位:它並非絕對不可打破的禁忌,而是潛在問題、系統風險的一種指示劑。
相比封裝與繼承,接口隔離原則與多態之間的關係更加直觀些。若是不使用多態,那麼咱們必定會違反接口隔離原則:當一個接口下只有一個實現類時,增長新的邏輯是都不免要增長接口方法,長此以往,這個接口就會變成一個巨無霸,接口隔離原則天然就無從談起了。反過來講,使用多態特性,咱們就應該遵照接口隔離原則、應該定義和使用「小而美」的接口。不然的話——設想一個聲明瞭十多個方法的接口,每次藉助多態特性來增長新的實現類時,咱們都不得不把全部方法都重寫一遍,那得多麼費勁!
固然,咱們仍然能夠藉助接口-基類-實現類的層次結構避免這個問題。但此時,咱們又回到了前面提到的那個問題上:「前者與後者並不知足繼承所要求的‘is-a’關係,此時咱們讓SubService繼承BaseService,真的不是撿起芝麻丟了西瓜嗎?」


接口隔離與其它設計原則
接口隔離與單一職責
接口隔離原則與單一職責原則之間的關係是顯而易見的:違反接口隔離原則,就必定會違反單一職責原則。
不管咱們把接口隔離原則定義爲「客戶端只須要依賴他們須要的接口」、仍是定義爲「把大接口拆分紅小接口」,只要違反了這一原則,接口內就勢必會出現不該出現的方法聲明。例如前面示例中反覆提到的接口實現步驟、其它模塊功能等。而接口方法通常都是抽象方法,必須由實現類重寫。在二者的疊加影響下,實現類中必定會出現本來不該出現的方法實現。即便咱們使用了接口-基類-實現類的層次結構,或者爲接口方法提供了默認方法體,也沒法解決這一問題:基類中已實現的方法,以及接口中的默認方法,都會被實現類繼承下來,成爲它本身的功能。這樣一來,實現類想要保持單一職責,就只能是個奢望了。
接口隔離原則與單一職責原則之間的這種關係,歸根結底的說,是接口與實現類之間的關係決定的:接口對外聲明瞭「我能作什麼」,實現類則爲接口提供了「怎麼作」的功能支撐。這就有點像產品和開發同樣:產品提需求,定義「這個產品能作什麼」;開發出設計、寫代碼,解決「怎麼作」的問題。
質量低下的產品需求是開發的一大痛苦之源;相似的,質量低下的接口定義也會給開發帶來無盡的痛苦。應付糟糕的產品需求已經讓人心力交瘁了,開發又何苦爲難本身呢?仍是認認真真遵照接口隔離原則、定義簡單清晰的接口吧!
接口隔離與開閉
在面向對象思想中,開閉原則的核心在於合理、高效地利用繼承和多態特性來「增長」新的實現類、而不是「修改」原有的實現類。所以,接口隔離原則與開閉原則之間的關係,須要繼承和多態來理解:明白了接口隔離原則與繼承、多態之間的關係,也就很容易理解它與開閉原則的關係了。
接口隔離與里氏替換
接口隔離原則主要討論接口的設計,而里氏替換原則則「下沉」到了繼承層次中,主要討論子類繼承父類時的問題。所以,兩者的關係與接口隔離和開閉之間的關係同樣,也須要繞道繼承和多態。



往期索引

《面向對象是什麼》

從具體的語言和實現中抽離出來,面向對象思想到底是什麼? 公衆號:景昕的花園面向對象是什麼


抽象

抽象這個東西,提及來很抽象,其實很簡單。

花園的景昕,公衆號:景昕的花園抽象


高內聚與低耦合

細說幾種內聚

細說幾種耦合

"高內聚"與"低耦合"是軟件設計和開發中常常出現的一對概念。它們既是作好設計的途徑,也是評價設計好壞的標準。

花園的景昕,公衆號:景昕的花園高內聚與低耦合


封裝

繼承

多態》

——「面向對象的三大特性是什麼?」——「封裝、繼承、多態。」


《[5+1]單一職責原則》

單一職責原則很是好理解:一個類應當只承擔一種職責。由於只承擔一種職責,因此,一個類應該只有一個發生變化的緣由。 花園的景昕,公衆號:景昕的花園[5+1]單一職責原則


《[5+1]開閉原則(一)

《[5+1]開閉原則(二)

什麼是擴展?就Java而言,實現接口(implements SomeInterface)、繼承父類(extends SuperClass),甚至重載方法(Overload),均可以稱做是「擴展」。什麼是修改?在Java中,嚴格來講,凡是會致使一個類從新編譯、生成不一樣的class文件的操做,都是對這個類作的修改。實踐中咱們會放寬一點,只有改變了業務邏輯的修改,纔會納入開閉原則所說的「修改」之中。 花園的景昕,公衆號:景昕的花園[5+1]開閉原則(一)


《[5+1]里氏替換原則(一)

《[5+1]里氏替換原則(二)

里氏替換原則(Liskov Substitution principle)是一條針對對象繼承關係提出的設計原則。它以芭芭拉·利斯科夫(Barbara Liskov)的名字命名。1987年,芭芭拉在一次名爲「數據的抽象與層次」的演講中首次提出這條原則;1994年,芭芭拉與另外一位女性計算機科學家周以真(Jeannette Marie Wing)合做發表論文,正式提出了這條面向對象設計原則

花園的景昕,公衆號:景昕的花園[5+1]里氏替換原則(一)


[5+1]接口隔離原則(一)

通常咱們會說,接口隔離原則是指:把龐大而臃腫的接口拆分紅更小、更具體的接口。不過,這並非接口隔離原則的定義。 實際上,接口隔離原則的定義實際上是這樣的…… 客戶端不該被迫依賴它們壓根用不上的接口; 或者反過來講,客戶端應該只依賴它們要用的接口。
花園的景昕,公衆號:景昕的花園[5+1]接口隔離原則(一)


景昕的花園.png

相關文章
相關標籤/搜索