前言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> { // 略}
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();
接口隔離與抽象
不少時候,咱們一提到抽象,就會直接把它與接口劃上等號。因此很天然的,談到接口隔離與抽象,咱們也會直接地想到把「接口隔離」與「更小的抽象」劃上等號。
這個觀點倒也沒有什麼大問題。尤爲是當接口隔離原則被簡化爲「把龐大而臃腫的接口拆分紅更小、更具體的接口」時,它與抽象之間的關係天然就只能是「把龐大而臃腫的抽象拆分爲更小、更具體的抽象」了。
例如,有時咱們會在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);}
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; }}
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>{}
接口隔離與高內聚低耦合
其實前面已經把接口隔離與高內聚低耦合之間的關係表述得很清楚了:適當地遵循接口隔離原則,有助於建立高內聚低耦合的抽象和模塊。
例如,把SomeService接口中的step1()/step2()等方法刪掉,只保留doSth()方法,不只遵循了接口隔離原則,也下降了服務調用者與提供者之間的耦合度。結合《細說幾種耦合》來看,這個改造至少能夠避免雙方產生內容耦合。
而把FlowService中關於用戶的功能拆分到UserService中,則能夠有效地提升對應模塊的內聚性:用戶相關功能和流程相關功能都放到各自的模塊中,內聚性至少能夠從偶然內聚提升到過程內聚甚至順序內聚(參考《細說幾種內聚》)。內聚性提升了,天然地,用戶模塊和流程模塊之間的耦合性也下降了。
雖然遵循接口隔離原則有助於提升內聚性、下降耦合性,可是「過猶不及」,過於強調接口隔離,有時反而會下降內聚、增長耦合。
例如,Java中的Iterator接口中,就有這樣兩個方法:ui
public interface Iterator<E> {
boolean hasNext();
E next();
// 其它方法,略}
接口隔離與封裝繼承多態
接口隔離原則與封裝的關係很是容易理解;相比之下,它與繼承、多態之間的關係就不那麼清晰了。
在面向對象中,接口是實現封裝特性的最有力也最多見的手段。與接口密切相關的接口隔離原則,天然也與封裝特性有着密切的關係。
相信咱們不少人都被「過分包裝」噁心過:實際的商品重不到三兩、大不過拳頭,非要左一層「精美包裝」、右一層「豪華包裝」。結果呢?買的人花一筆冤枉錢買了個不痛快,用的人拆一大堆空盒子用得不痛快。哪怕是用來收禮,若是知道這「禮物」的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(){ // 實現本身所需方法 }}
接口隔離與其它設計原則
接口隔離與單一職責
接口隔離原則與單一職責原則之間的關係是顯而易見的:違反接口隔離原則,就必定會違反單一職責原則。
不管咱們把接口隔離原則定義爲「客戶端只須要依賴他們須要的接口」、仍是定義爲「把大接口拆分紅小接口」,只要違反了這一原則,接口內就勢必會出現不該出現的方法聲明。例如前面示例中反覆提到的接口實現步驟、其它模塊功能等。而接口方法通常都是抽象方法,必須由實現類重寫。在二者的疊加影響下,實現類中必定會出現本來不該出現的方法實現。即便咱們使用了接口-基類-實現類的層次結構,或者爲接口方法提供了默認方法體,也沒法解決這一問題:基類中已實現的方法,以及接口中的默認方法,都會被實現類繼承下來,成爲它本身的功能。這樣一來,實現類想要保持單一職責,就只能是個奢望了。
接口隔離原則與單一職責原則之間的這種關係,歸根結底的說,是接口與實現類之間的關係決定的:接口對外聲明瞭「我能作什麼」,實現類則爲接口提供了「怎麼作」的功能支撐。這就有點像產品和開發同樣:產品提需求,定義「這個產品能作什麼」;開發出設計、寫代碼,解決「怎麼作」的問題。
質量低下的產品需求是開發的一大痛苦之源;相似的,質量低下的接口定義也會給開發帶來無盡的痛苦。應付糟糕的產品需求已經讓人心力交瘁了,開發又何苦爲難本身呢?仍是認認真真遵照接口隔離原則、定義簡單清晰的接口吧!
接口隔離與開閉
在面向對象思想中,開閉原則的核心在於合理、高效地利用繼承和多態特性來「增長」新的實現類、而不是「修改」原有的實現類。所以,接口隔離原則與開閉原則之間的關係,須要繼承和多態來理解:明白了接口隔離原則與繼承、多態之間的關係,也就很容易理解它與開閉原則的關係了。
接口隔離與里氏替換
接口隔離原則主要討論接口的設計,而里氏替換原則則「下沉」到了繼承層次中,主要討論子類繼承父類時的問題。所以,兩者的關係與接口隔離和開閉之間的關係同樣,也須要繞道繼承和多態。
往期索引
從具體的語言和實現中抽離出來,面向對象思想到底是什麼? 公衆號:景昕的花園面向對象是什麼
《抽象》
抽象這個東西,提及來很抽象,其實很簡單。
花園的景昕,公衆號:景昕的花園抽象
《高內聚與低耦合》
《細說幾種內聚》
《細說幾種耦合》
"高內聚"與"低耦合"是軟件設計和開發中常常出現的一對概念。它們既是作好設計的途徑,也是評價設計好壞的標準。
花園的景昕,公衆號:景昕的花園高內聚與低耦合
《封裝》
《繼承》
《多態》
——「面向對象的三大特性是什麼?」——「封裝、繼承、多態。」
單一職責原則很是好理解:一個類應當只承擔一種職責。由於只承擔一種職責,因此,一個類應該只有一個發生變化的緣由。 花園的景昕,公衆號:景昕的花園[5+1]單一職責原則
什麼是擴展?就Java而言,實現接口(implements SomeInterface)、繼承父類(extends SuperClass),甚至重載方法(Overload),均可以稱做是「擴展」。什麼是修改?在Java中,嚴格來講,凡是會致使一個類從新編譯、生成不一樣的class文件的操做,都是對這個類作的修改。實踐中咱們會放寬一點,只有改變了業務邏輯的修改,纔會納入開閉原則所說的「修改」之中。 花園的景昕,公衆號:景昕的花園[5+1]開閉原則(一)
里氏替換原則(Liskov Substitution principle)是一條針對對象繼承關係提出的設計原則。它以芭芭拉·利斯科夫(Barbara Liskov)的名字命名。1987年,芭芭拉在一次名爲「數據的抽象與層次」的演講中首次提出這條原則;1994年,芭芭拉與另外一位女性計算機科學家周以真(Jeannette Marie Wing)合做發表論文,正式提出了這條面向對象設計原則
花園的景昕,公衆號:景昕的花園[5+1]里氏替換原則(一)
通常咱們會說,接口隔離原則是指:把龐大而臃腫的接口拆分紅更小、更具體的接口。不過,這並非接口隔離原則的定義。 實際上,接口隔離原則的定義實際上是這樣的…… 客戶端不該被迫依賴它們壓根用不上的接口; 或者反過來講,客戶端應該只依賴它們要用的接口。
花園的景昕,公衆號:景昕的花園[5+1]接口隔離原則(一)