六大設計原則--接口隔離原則【 Interface Segregation Principle】

聲明:本文內容是從網絡書籍整理而來,並非原創。

定義

  • 第一種定義:

    Clients should not be forced to depend upon interfaces that they don’t use.
    客戶端不應該依賴它不需用的接口。

  • 第二種定義:

    The dependency of one class to another one should depend on the smallest possible interface。
    類間的依賴關係應該建立在最小的接口上。

    兩種定義如出一撤,只是對一個事物的兩種不同描述。
    我們可以把這兩個定義概括爲一句話:建立單一接口,不要建立臃腫龐大的接口。再通俗的一點講:接口儘量細化,同時接口中的方法儘量的少。看到這兒大家可能會疑惑,這與之前的單一職責原則不是相同的嗎?錯,接口隔離原則與單一職責的定義的規則是不相同的,單一職責要求的是類和接口職責單一,注重的是職責,沒有要求接口的方法減少,例如一個職責可能包含 10 個方法,這 10 個方法都放在一個接口中,並且提供給多個模塊訪問,各個模塊按照規定的權限來訪問,在系統外通過文檔約束不使用的方法不要訪問,按照單一職責原則是允許的,按照接口隔離原則是不允許的,因爲它要求“儘量使用多個專門的接口”,專門的接口指什麼?就是指提供給多個模塊的接口,提供給幾個模塊就應該有幾個接口,而不是建立一個龐大的臃腫的接口,所有的模塊可以來訪問。

美女與星探的例子

我們來定義一下什麼是美女:1.面貌好看,2.身材要窈窕,3.要有氣質。我們用類圖類體現一下星探找美女的過程:
這裏寫圖片描述
美女的定義:

public interface IPettyGirl {
    //要有姣好的面孔
    public void goodLooking();
    //要有好身材
    public void niceFigure();
    //要有氣質
    public void greatTemperament();
}

美女的實現類:

public class PettyGirl implements IPettyGirl {
    private String name;
    //美女都有名字
    public PettyGirl(String _name){
        this.name=_name;
    }
    //臉蛋漂亮
    public void goodLooking() {
        System.out.println(this.name + "---臉蛋很漂亮!");
    }
    //氣質要好
    public void greatTemperament() {
        System.out.println(this.name + "---氣質非常好!");
    }
    //身材要好
    public void niceFigure() {
        System.out.println(this.name + "---身材非常棒!");
    }
}

然後我們來看 AbstractSearcher 類,這個類就是指星探這個行業了,代碼如下:

public abstract class AbstractSearcher {
    protected IPettyGirl pettyGirl;
    public AbstractSearcher(IPettyGirl _pettyGirl){
        this.pettyGirl = _pettyGirl;
    }
    //搜索美女,列出美女信息
    public abstract void show();
}

星探查找到美女,打印出美女的信息,代碼如下:

public class Searcher extends AbstractSearcher{
    public Searcher(IPettyGirl _pettyGirl){
        super(_pettyGirl);
    }
    //展示美女的信息
    public void show(){
        System.out.println("--------美女的信息如下: ---------------");
        //展示面容
        super.pettyGirl.goodLooking();
        //展示身材
        super.pettyGirl.niceFigure();
        //展示氣質
        super.pettyGirl.greatTemperament();
    }
}

場景中的兩個角色美女和星探都已經完成了,我們再來寫個場景類,展示一下我們的這個過程:

public class Client {
    //搜索並展示美女信息
    public static void main(String[] args) {
        //定義一個美女
        IPettyGirl yanYan = new PettyGirl("嫣嫣");
        AbstractSearcher searcher = new Searcher(yanYan);
        searcher.show();
    }
}

運行結果如下:

--------美女的信息如下: ---------------
嫣嫣---臉蛋很漂亮!
嫣嫣---身材非常棒!
嫣嫣---氣質非常好!

星探尋找美女的程序我們就開發完畢了,我們來想想這個程序有沒有問題,思考一下 IPettyGirl 這個接口,這個接口是否做到了最優秀的設計。

我們的審美觀點都在改變,美女的定義也在變化。一千多年前的唐朝楊貴妃如果活在現代這個年代非羞愧死不行,爲什麼?胖呀!但是胖不不影響她入選中國的四大美女行列,說明當時的審美和現在是有差異地,當然隨着時代的發展我們的審美觀也在變化,就現在,你發現有一個女孩,臉蛋不怎麼樣,身材也一般般,但是氣質非常好,大部分人也會把這樣的女孩叫美女,審美素質提升了,但是我們接口卻定義了美女必須是三者都具備呀,可能你要說了,我重新擴展一個美女類,只實現 greatTemperament 方法其他兩個方法置空,什麼都不寫,不就可以了嗎?聰明,但是行不通!爲什麼呢?星探 AbstractSearcher 依賴的是 IPettyGirl 接口,它有三個方法,你只實現了兩個方法,星探的方法是不是要修改?我們上面的程序打印出來的信息少了兩條,還讓星探怎麼去辨別是不是美女呢?好了,我們發現我們的接口 IPettyGirl 接口設計是有缺陷地,過於龐大了,容納了一些可變的因素,根據接口隔離原則,星探 AbstractSearcher 應該依賴與具有部分特質的女孩子,而我們卻把這些特質都封裝了起來,放到了一個接口中了,封裝過渡了!問題查找到了,我們重新修改一下類圖:
這裏寫圖片描述
把原 IPettyGirl 接口拆分爲兩個接口,一種是外形美的美女 IGoodBodyGirl,這類美女的特點就是臉蛋和身材極棒,超一流,但是沒有審美素質,比如隨地吐痰,出口就是 KAO,CAO 之類的,文化程度比較低;另外一種是氣質美的美女 IGreatTemperamentGirl,談吐和修養都非常高。我們從一個比較臃腫的接口拆分成了兩個專門的接口,靈活性提高了,可維護性也增加了,不管以後是要外形美的美女還是氣質美的美女都可以輕鬆的通過 PettyGirl 定義。我們先看兩種類型的美女接口:

public interface IGoodBodyGirl {
    //要有姣好的面孔
    public void goodLooking();
    //要有好身材
    public void niceFigure();
}
public interface IGreatTemperamentGirl {
    //要有氣質
    public void greatTemperament();
}

實現類沒有改變,只是實現類兩個接口,代碼如下:

public class PettyGirl implements IGoodBodyGirl,IGreatTemperamentGirl {
    private String name;
    //美女都有名字
    public PettyGirl(String _name){
        this.name=_name;
    }
    //臉蛋漂亮
    public void goodLooking() {
        System.out.println(this.name + "---臉蛋很漂亮!");
    }
    //氣質要好
    public void greatTemperament() {
        System.out.println(this.name + "---氣質非常好!");
    }
    //身材要好
    public void niceFigure() {
        System.out.println(this.name + "---身材非常棒!");
    }
}

通過這樣的改造以後,不管以後是要氣質美女還是要外形美女,都可以保持接口的穩定。當然你可能要說了,以後可能審美觀點再發生改變,只有臉蛋好看就是美女,那這個 IGoodBody 接口還是要修改的呀,確實是,但是設計時有限度的,不能無限的考慮未來的變更情況,否則就會陷入設計的泥潭中而不能自拔。

以上把一個臃腫的接口變更爲兩個獨立的接口依賴的原則就是接口隔離原則,讓 AbstractSearcher 依賴兩個專用的接口比依賴一個綜合的接口要靈活。接口是我們設計時對外提供的契約,通過分散定義多個接口,可以預防未來變更的擴散,提高系統的靈活性和可維護性。

接口隔離原則包含的四層含義

  1. 接口儘量要小。這是接口隔離原則的核心定義,不出現臃腫的接口(Fat Interface),但是“小”是有限度的,首先就是不能違反單一職責原則, 什麼意思呢?我們在單一職責原則中提到一個 IPhone 的例子,在這裏例子中我們使用單一職責原則把在一個接口中的方法分解到兩個接口中,類圖如下:
    這裏寫圖片描述
    我們想想 IConnectManager 接口是否還可以再繼續拆分下去,掛電話有兩種方式:一種是正常的電話掛掉,一種是手機突然沒電了,通訊當然就斷了,這兩種方式的處理應該是不同的,爲什麼呢?正常掛電話,對方接受到掛機信號,計費系統也就停止計費了,那手機沒電了這種方式就不同了,它是信號丟失了,中繼服務器檢查到了,然後通知計費系統停止計費,否則你的費用不是要瘋狂的增長了嗎?!思考到這裏,我們是不是就要動手把 IConnectManager 接口拆封成兩個, 一個是負責連接的,一個接口是負責掛電話的?是要這樣做嗎?且慢,讓我們再思考一下,如果拆分了,那就不符合單一職責原則了,因爲從業務上來講通訊的建立和關閉已經是最小的業務單位了,再細分下去就是對業務或是協議(其他業務邏輯)的拆解了,想想看一個電話要關心 3G 協議,要考慮中繼服務器等等,這個電話還怎麼做的出來呢?從業務層次來看這樣的設計就是一個失敗的設計。一個原則要拆,你原則又不要拆,那怎麼辦?好辦, *根據接口隔離原則拆分接口時,必須首先滿足單一職責原則

  2. 接口要高內聚。 什麼是高內聚?高內聚就是提高接口、類、模塊的處理能力,減少對外的交互,就比如一個人,你告訴下屬“到奧巴馬的辦公室偷一個 XX 文件”,然後就聽到下屬就堅定的口吻回答你“好的,保證完成!”,然後一個月後還真的把 XX 文件放到你的辦公桌了,這種不講任何條件、立刻完成任務的行爲就是高內聚的表現。具體到接口隔離原則就是要求在接口中儘量少公佈 public 方法,接口是對外的承諾,承諾越少對系統的開發越有利,變更的風險也就越少,同時也有利於降低成本。

  3. 定製服務。 一個系統或系統內的模塊之間必然會有耦合,有耦合就要相互訪問的接口(並不一定就是Java 中定義的 Interface,也可能是一個類或者是單純的數據交換),我們設計時就需要給各個訪問者(也就客戶端)定製服務,什麼是定製服務?你到商場買衣服,找到符合自己身體的尺碼的衣服就成了,基本上就不會差別太大,可能是前鬆後緊,晚上睡不着覺之類的不太合適,但是好歹也是個衣服,能穿。如果你到裁縫店裏做衣服會是什麼樣子呢?裁縫會幫你量腰圍,胸圍,肩寬等等,然後做出一件衣服,這件衣服肯定非常符合你的身體,那這就是定製服務,單獨爲一個個體提供優良優良的服務。我們在做系統設計時也需要考慮對系統之間或模塊之間的定義要採用定製服務,採用定製服務就必然有一個要求就是:只提供訪問者需要的方法,這是什麼意思?我們舉個例子來說明,比如我們做了一個圖書管理系統,其中有一個查詢接口的,方便管理員查詢圖書,其類圖如下:
    這裏寫圖片描述
    在接口中定義了多個方法,分別可以按照作者,標題,出版社,分類查詢,最後還提供了混合查詢方式,程序也寫好了,投產上線了,突然有一天發現系統速度非常慢,然後就開始痛苦的分析,最終發現是通過這個接口的 complexSearch(Map map)方法併發量太大,導致應用服務器性能下降,然後繼續跟蹤下去才發現這些查詢都是從公網上發起的,進一步分析,發現問題了:提供給公網(公網項目是另外一個項目組開發的)的查詢接口和提供給系統內管理人員的接口是相同的,都是 IBookSearcher 接口,但是權限不同,系統管理人員可以通過這個接口查詢到所有的書籍,而公網的這個方法是被限制的,不返回任何值的,在設計時通過口頭約束,這個方法是不可被調用的,但是由於公網項目組的疏忽,這個方法還是公佈了出去,雖然不能返回結果,但是還是引起了應用服務器的性能巨慢的情況發生,這就是活生生的一起接口臃腫引起性能部長的案例。
    問題找到了,我們就把這個接口進行重構:把 IBookSearcher 拆分爲兩個接口,如下圖:
    這裏寫圖片描述
    提供給管理人員的實現類同時實現 ISimpleBookSearcher 和 IComplexBookSearcher 兩個接口,原有程序不用任何改變,而提供給公網的接口變爲 ISimpleBookSearcher,只允許進行簡單的查詢,單獨爲它定製服務。

  4. 接口設計是有限度的。接口的設計粒度是越小系統越靈活,這是不爭的事實,但是這就帶來的結構的複雜化,開發難度增加,維護性降低,這不是一個項目或產品所期望看到的,所有接口設計一定要注意適度,適度的“度”怎麼來判斷的呢?根據經驗和常識判斷!

幾個劃分接口粒度的規則

  • 一個接口只服務於一個子模塊或者業務邏輯
  • 通過業務邏輯壓縮接口中的 public 方法。接口時常去回顧,儘量做讓接口達到“滿身筋骨肉”,而不是“肥嘟嘟”的一大堆方法。
  • 已經被污染了的接口,儘量去修改,若變更的風險較大,則採用適配器模式進行轉化處理
  • 瞭解環境,拒絕盲從。 每個項目或產品都有特定的環境因素,別看到大師是這樣做的你就照抄,千萬別,環境不同的,接口拆分的標準就不同。深入瞭解的業務邏輯,最好的接口設計就出自的你的手!

接口隔離原則和其他的設計原則一樣,都是需要花費較多的時間和精力來進行設計和籌劃,但是它帶來了設計的靈活性,讓你在業務人員在提出“無理”要求的時候可以輕鬆應付。貫徹使用接口隔離原則最好的方法就是一個接口一個方法,保證絕對符合接口隔離原則(有可能不符合單一職責原則),但你會採用嗎?!不會,除非你是瘋子!那怎麼才能正確的使用接口隔離原則呢? 答案是根據經驗和常識決定接口的粒度大小,接口粒度太小,導致接口數據劇增,開發人員嗆死在接口的海洋裏;接口粒度太大,靈活性降低,無法提供定製服務,給整體項目帶來無法預計的風險。

怎麼準確的實踐接口隔離原則?一句話:實踐,經驗和領悟!

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