天天看點

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

前言

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

[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

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

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

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

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

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

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

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

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

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

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

↑奧卡姆剃刀↑

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

為什麼

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

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

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

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

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

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);
}      

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

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

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

↑有誰還會唱這首歌嗎↑

怎麼做

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

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

// 把第一個接口,拆分成兩個接口
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]裡氏替換原則(一)