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

前言

面向對象的SOLID設計原則,外加一個迪米特法則,就是我們常說的5+1設計原則。

圖片↑ 五個,再加一個,就是5+1個。哈哈哈。


這六個設計原則的位置有點不上不下。

論原則性和理論指導意義,它們不如封裝繼承抽象或者高內聚低耦合,所以在寫代碼或者code review的時候,它們很難成爲“應該這樣做”或者“不應該這樣做”的一個有說服力的理由。論靈活性和實踐操作指南,它們又不如設計模式或者架構模式,所以即使你能說出來某段代碼違反了某項原則,常常也很難明確指出錯在哪兒、要怎麼改。

所以,這裏來討論討論這六條設計原則的“爲什麼”和“怎麼做”。順帶,作爲面向對象設計思想的一環,這裏也想聊聊它們與抽象、高內聚低耦合、封裝繼承多態之間的關係。

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


是什麼

一般我們會說,接口隔離原則是指:把龐大而臃腫的接口拆分成更小、更具體的接口。


不過,這並不是接口隔離原則的定義。實際上,接口隔離原則的定義其實是這樣的:

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

The Interface Segregation Principle

https://drive.google.com/file/d/0BwhCYaYDn8EgOTViYjJhYzMtMzYxMC00MzFjLWJjMzYtOGJiMDc5N2JkYmJi/view

也就是說,客戶端不應被迫依賴它們壓根用不上的接口;或者反過來說,客戶端應該只依賴它們要用的接口。

圖片

↑接口隔離原則的準確定義↑

這裏的“接口”有一點迷惑性。雖然命名和定義中討論的都是“接口”,但是這裏的接口並非我們代碼中的interface,而是粒度更細緻的“接口方法”。例如,我們有這樣一段代碼:

public interface SomeInterface{
    Dto query(Queryer queryer);
    int update(Queryer queryer, Dto data);
}


從interface的角度來看,這段代碼只聲明瞭一個接口。但是,從“接口方法”的角度來看,這段代碼聲明瞭兩個接口:一個用於查詢數據,一個用於更新數據。如果一個客戶端——例如QueryDataController——只需要使用其中的query()方法,那麼對它來說,雖然SomeInterface是一個必要的依賴項,但是update()方法卻不是。

另外一個令我感到迷惑的是,接口隔離原則的命名與定義實在有點有點名不副實。它的命名說的是“怎麼做”,而並不是概括“做什麼”;而它的定義雖然提到了“接口”,可是卻閉口不談“隔離”。這就使得接口隔離原則不能像其它設計原則那樣顧名思義。如果是我的話,也許會把這一原則命名爲“最小依賴原則”或者“必要依賴原則”。

不過,如果這樣命名的話,那麼這一設計原則的指向性又有點太模糊了。除了接口隔離之外,我們還有很多種辦法可以爲客戶端“減負”:例如以後會提的迪米特法則、門面模式等,都可以實現這一目標。也許,就是考慮到區分度,所以才把這個“最小依賴原則”稱爲“接口隔離原則”吧。

此外,接口隔離原則的定義可謂別有深意。它總讓我想起著名的“奧卡姆剃刀”法則:如無必要,勿增實體。實際上,接口隔離原則也是“奧卡姆剃刀”法則的一種應用:如無必要,勿增接口依賴。如果覺得接口隔離原則的說服力不太夠,可以試試扛出這把“奧卡姆剃刀”來。


圖片↑奧卡姆剃刀↑

岔開說一句,有的時候真的覺得“天地有道、萬物一理”這話很有道理。例如,同樣的一條道理,我們可以總結爲“如無必要勿增實體”,也可以總結爲“接口隔離原則”,還可以表述爲“less is more”、“句有可削,足見其疏;字不得減,乃知其密”、“斷舍離”、甚至是“簡約而不簡單”。不得不說,世界真奇妙。


爲什麼

其實,如果從正面來考慮“遵守接口隔離原則有什麼好處”,恐怕我們很難得到令人信服的答案。因爲接口隔離原則和“奧卡姆剃刀”原則類似,並不是邏輯上不可辯駁的定理或結論,而只能作爲啓發式技巧來幫助我們發展模型。

但是,如果從反面來論述“違反接口隔離原則有什麼壞處”,就很容易理解了。“違反接口隔離原則”就像消失的地衣、或者變色的石蕊試紙一樣,提示着我們“這裏似乎有點問題”。

圖片

↑還記得這漂亮的小彩紙麼↑

例如,我們有這樣一個接口:

public interface FlowService{
    Flow approve(Flow curFlow);    
    User queryUser(Long userId);
}


這個接口的怪異之處不言而喻:一個流程審批的方法,和一個查詢用戶信息的方法,怎麼會出現在同一個接口裏呢?我們很難推斷箇中緣由。看可以肯定,這個接口違反了接口隔離原則:一個只需要處理流程審批的調用者,纔不關心怎樣查詢用戶信息呢。

由此我們還會發現,這個接口的實現類也被迫違反單一職責原則:它不僅要承擔流程審批的職責,還要承擔查詢用戶的職責。由此,這些實現類也就變得低內聚、高耦合了起來。也許在某個時刻,這種“大雜燴”式的接口能給我們帶來一時的便利;但是長遠來看,它一定會成爲系統擴展、演化路上的絆腳石。

當然,現在絕大多數程序員都不會再寫這種“大雜燴”接口了。不過,我們還能見到一些其它的違反了接口隔離原則的情況。

例如,我經常見到這樣的接口:

public interface SomeService{
    void doSth(Dto data);    
    void step1(Dto data);    
    void step2(Dto data);    
    void step3(Dto data);
}


這個接口定義了四個方法。其中,只有doSth(Dto)方法是提供給外部使用的;其餘step1(Dto)/step2(Dto)/step3(Dto)方法,都只是doSth(Dto)方法的中間步驟,僅在SomeService實現類中被調用。

雖然這四個方法都是爲了同一個功能服務的,但是,這個接口還是違反了接口隔離原則:一個調用者只需要知道這個接口能做什麼——也就是隻需要調用doSth()方法,但並不需要、也不應該關心doSth()方法分了幾個步驟、每一個步驟是什麼。

由此我們可以說,這個接口不是一個合格的抽象,因爲它把接口方法的實現細節暴露了出來。同時,它也不夠“高內聚低耦合”。而且,如果某個實現類脫離了這種“三個步驟”的框架,那這個接口反而成了擴展的阻礙。可見,這個接口對“開閉原則”的支持也不夠好。還有……

還有這樣的接口:

public interface UserService{
    User queryById(Long userId);    
    User queryByIdCard(String idCard);    
    User queryByPhone(String phone);    
    void registerByEmail(User user);    
    void reigsterByPhone(User user, String verifyCode);
}


相比前面兩類接口,這種接口恐怕最爲司空見慣的——但是,未必是恰當的。它同樣向調用方透露了太多不必要的信息,同樣違反了接口隔離原則。同樣的,這個接口也不是一個合格的抽象,也不夠“高內聚低耦合”,也不夠“開閉”;而且它的實現類肯定會違反單一職責原則;如果實現類的子類寫得不夠用心,還很容易違反里氏替換原則(然而如果用心寫,又不得不付出額外的心血)……

我們很難說這些問題全都是因爲這些接口違反了接口隔離原則。它們之間也許沒有因果關係,但一定有很強的關聯關係。就好像母雞下蛋時一定會“咯咯噠”地叫一樣:很難說清二者之間的因果關係,但我們都知道,母雞“咯咯噠”地叫了,我們就有雞蛋吃了。

圖片

↑有誰還會唱這首歌嗎↑


怎麼做

相比其它原則,遵守接口隔離原則實在是太容易了:把接口中多餘的部分“剔除”掉,比如拆分到其它接口中去,或者隱藏到接口內部去,就可以了。

例如,前面例子中的第一個接口,就可以修改成這樣:

// 把第一個接口,拆分成兩個接口
public interface FlowService{
    Flow approve(Flow curFlow);
}
public interface UserService{
    User queryUser(Long userId);
}


簡單的一次拆分,就可以讓新的接口遵循接口隔離原則,讓“凱撒的歸凱撒,上帝的歸上帝”了。

第二個接口的改造更簡單一些;不過,考慮到爲接口方法定義實現步驟的需求,我們還需要一個實現類:

public interface SomeService{
    void doSth(Dto data);
}
public abstract class SomeServiceAsSkeleton{
    public void doSth(Dto data){
         step1(data);        
         step2(data);        
         step3(data);    
     }    
     protected abstract void step1(Dto data);    
     protected abstract void step2(Dto data);    
     protected abstract void step3(Dto data);
 }


這是模板模式的常見寫法,想必原先的作者也是想使用模板模式吧。不過,接口定義的是對接口外部提供的功能,而抽象類定義的纔是內部子類的基礎實現。後者不需要、也不應該放到接口中。

第三個接口的改造還要更復雜一些:它的接口固然可以簡單地合併成一個,但是考慮到不同情況下需要使用不同的查詢參數,它的實現類還需要多花費些心思:

public interface UserService{
    /**根據入參中的不同數據,使用不同的查詢條件*/    
    User queryUser(UserQuery query);
}

public interface UserRegster<T extends UserRegDto>{
    /**不同的子類使用不同的數據和實現*/    
    void register(T user);
}
public class UserQuery{
     private Long userId;     
     private String idCard;     
     private String phone;     
     private String email;    
     // getter和setter略
}
public class UserRegDto{
    private Long userId;     
    private String idCard;
}
public class UserRegByEmailDto extends UserRegDto{
     private String email;
}
public class UserRegByPhoneDto extends UserRegDto{
     private String phone;     
     private String verifyCode;
 }


總之,如果只是遵循接口隔離原則,接口設計確實挺簡單。不過,再和其它方方面面綜合起來考慮的話,這個簡單的接口設計確實也不太簡單。說到底,接口代表的是功能抽象,而非簡單的interface,還應該認真對待。


往期索引

《面向對象是什麼》

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


抽象

抽象這個東西,說起來很抽象,其實很簡單。

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


高內聚與低耦合

細說幾種內聚

細說幾種耦合

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

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


封裝

繼承

多態》

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



《[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]里氏替換原則(一)

qrcode.bmp

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章