面向對象設計之高內聚、低耦合【轉】

內聚

內聚的含義內聚指一個模塊內部元素彼此結合的緊密程度java

 

在面向對象領域,談到「內聚」的時候,模塊的概念遠遠不止咱們一般所理解的「系統內的某個模塊」這個範圍,而是可大可小,大到一個子系統,小到一個函數,你均可以理解爲內聚數據庫

裏所說的 「模塊」。因此能夠用「內聚」來判斷一個函數設計是否合理,一個類設計是否合理,一個接口設計是否合理, 一個包設計是否合理,一個模塊/子系統設計是否合理。數據結構

其次:「元素」到底是什麼? 有了前面對「模塊」的深刻研究後,元素的含義就比較容易明確了(不一樣語言稍有不一樣)。ide

函數:函數的元素就是「代碼」函數

類/接口:類的元素是「函數、屬性」學習

包:包的元素是「類、接口、全局數據」等測試

模塊:模塊的元素是「包、命名空間」編碼

 

再次:「結合」是什麼? 英文的原文是「belong」,有「屬於」的意思,翻譯成中文「結合」,更加貼近中文的理解。但「結合」本 身這個詞容易引發誤解。絕大部分人看到「結合」這個單詞,spa

想到的確定是「你中有我、我中有你」這樣 的含義,甚至可能會聯想到「美女和帥哥」的結合,抑或「青蛙王子和公主」的結合這種狀況。這樣的理解自己也並無錯,但比較狹隘。 咱們翻譯

以類的設計爲例:假如一個類裏面的函數都是隻依賴本類其它函數(固然不能循環調用啦),那內聚性確定是最好的,由於「結合」得很緊密

判斷一個模塊(函數、類、包、子系統)「內聚性」的高低,最重要的是關注模塊的元素是否都忠於模塊的職責,簡單來講就是「不要掛羊頭賣狗肉」。

 

【內聚的分類】 

如下各類形式的內聚的內聚性愈來愈高

【巧合內聚(Coincidental cohesion)】

模塊內部的元素之因此被劃分在同一模塊中,僅僅是由於「巧合」!

這是內聚性最差的一種內聚,從名字上也能夠看出,模塊內的元素沒有什麼關係,元素自己的職責也各不 相同。基本上能夠認爲這種內聚形式其實是沒有內聚性。

但在實際應用中,這種內聚也是存在的,最多見的莫過於相似「Utils」這樣的包,或者「Miscellaneous」 這樣的包。

例如,以下樣例中,包package com.oo.cohesion.utils 即咱們的「模塊」,每一個類即咱們的「元 素」 。能夠看出,HtmlUtil、ImageUtil、StringUtil、UrlUtil都屬於同一個包

utils,但從類名稱所體 現的職責來看,相互間並沒有多大關係,也沒有明確的凝聚力。

HtmlUtil.java

package com.oo.cohesion.utils;
public class HtmlUtil {
}

ImageUtil.java

package com.oo.cohesion.utils;
public class ImageUtil {
}

StringUtil.java 

package com.oo.cohesion.utils;
public class StringUtil {
}

UrlUtil.java

package com.oo.cohesion.utils;
public class UrlUtil {
}

【邏輯內聚(Logical cohesion)】

模塊內部的元素之因此被劃分在同一模塊中,是由於這些元素邏輯上屬於同一個比較寬泛的類別!

模塊的元素邏輯上都屬於一個比較寬泛的類別,但實際上這些元素的職責可能也是不同的。 例如將「鼠標」和「鍵盤」劃分爲「輸入」類,將「打印機「、「顯示器」等劃分爲「輸出」類。

相比巧合內聚來講,邏輯內聚的元素之間仍是有部分凝聚力的,只是這個凝聚力比較弱,但比巧合內聚來 說要強一些。

例如,以下樣例中,包package com.oo.cohesion.input即咱們的模塊,每一個類即咱們的元素。能夠 看出,Mouse、Keyboard、Microphone都是輸入設備的一種,這是它們的

凝聚力所在,但這這些類自己 的職責是徹底不一樣的。

Keyboard.java

package com.oo.cohesion.input;
public class Keyboard {
}

Mouse.java

package com.oo.cohesion.input;
public class Mouse {
}

【時序內聚】

模塊內部的元素之因此被劃分在同一模塊中,是由於這些元素必須按照固定的「時間順序」進行處理。

這種內聚通常在函數這個級別的模塊中比較常見,例如「異常處理」操做,通常的異常處理都是「釋放資 源(例如打開的文件、鏈接、申請的內存)、記錄日誌、通知用戶」,那麼把這

幾個處理封裝在一個函數中, 它們之間的內聚就是「時序內聚」。

 

【過程內聚(Procedural cohesion)】

模塊內部的元素之因此被劃分在同一模塊中,是由於這些元素必須按照固定的「過程順序」進行處理。

過程內聚和時間內聚比較類似,也是在函數級別的模塊中比較常見。例如讀寫文件的操做,通常都是按照 這樣的順序進行的:判斷文件是否存在、判斷文件是否有權限、打開文件、

讀(或者寫)文件,那麼把這 些處理封裝在一個函數中,它們之間的內聚就是「過程內聚」。


【交互內聚(Communicational cohesion)】

模塊內部的元素之因此被劃分在同一模塊中,是由於這些元素都操做相同的數據。

交互內聚的名稱和定義差異較大,我也不知道老外爲啥這樣命名,其實我以爲還不如叫「共享內聚」 :) 雖然咱們以爲命名不太正確,但爲了交流的時候不至於引發誤會,咱們仍是

使用交互內聚來講明。

交互內聚最多見的就是數據結構的類定義了,例如Java HashMap的get、put、clear等操做。


【順序內聚(Sequential cohesion)】

模塊內部的元素之因此被劃分在同一模塊中,是由於某些元素的輸出是另外元素的輸入。

順序內聚其實就像一條流水線同樣,上一個環節的輸出是下一個環節的輸入。最多見的就是「規則引擎」 一類的處理,一個函數負責讀取配置,將配置轉換爲執行指令;另一個函數

負責執行這些指令。

例如,以下樣例中,包package com.oo.cohesion.ruleengin即咱們的模塊,每一個類即咱們的元素。 能夠看出,Parser類的輸出正好是Process類的輸入。

Parser.java

package com.oo.cohesion.ruleengin;
/** 
* 規則引擎解析類:解析配置文件,生成執行計劃ExecutePlan
*
*/

public class Parser {   public ExecutePlan parse(String configFile){ ExecutePlan plan = new ExecutePlan();     //...... //TODO:讀取配置文件,生成ExecutePlan對象 //......     return plan;   } }

Process.java

package com.oo.cohesion.ruleengin;
/** 
* 規則引擎執行類:執行輸入的執行計劃 ExecutePlan,返回執行結果
*
*/

public class Process {   public int process(ExecutePlan plan){ //TODO:執行規則引擎的指令 return 0; } }

ExecutePlan.java 

package com.oo.cohesion.ruleengin;
import java.util.ArrayList;
/** 
  * 執行計劃類:包含規則引擎執行相關的信息
  *
  */
 public class ExecutePlan { 
    public ArrayList codes = new ArrayList(); //指令序列
 }

【功能內聚(Functional cohesion)】

模塊內部的元素之因此被劃分在同一模塊中,是由於這些元素都是爲了完成同一項任務。功能內聚是內聚性最好的一種方式,但在實際操做過程當中,對因而否知足功能內聚並不很好

判斷,緣由在 於「同一項任務」這個定義也是比較模糊的。好比說咱們前面各類內聚方式的解讀中涉及的樣例,理解功能內聚的關鍵,仍是在於到底什麼是「同一項任務」。 關於「同一項

任務」這個定義的理解,其實另一個地方也會涉及到,那就是類的設計原則SRP(下一篇會詳細講解)。

耦合

耦合的含義:耦合(或者稱依賴)是程序模塊相互之間的依賴程度。

從定義來看,耦合和內聚是相反的:內聚關注模塊內部的元素結合程度,耦合關注模塊之間的依賴程度。

理解耦合的關鍵有兩點:什麼是模塊,什麼是依賴。

什麼是模塊? 模塊和內聚裏面提到的模塊同樣,耦合中的模塊其實也是可大可小。常見的模塊有:函數、類、包、子模 塊、子系統等

什麼是依賴? 依賴這個詞很好理解,通俗的講就是某個模塊用到了另一個模塊的一些元素。 例如:A類使用了B類做爲參數,A類的函數中使用了B類來完成某些功能等等

【耦合的分類】

【無耦合(No coupling)】

無耦合意味着模塊間沒有任何關係或者交互。


【消息耦合(Message coupling (low))】

模塊間的耦合關係表如今發送和接收消息。

這裏的「消息」隨着「模塊」的不一樣而不一樣,例如: 系統/子系統:兩個系統交互的協議數據,例如:HTTPPOST數據,Java RPC、Socket數據等; 類/函數:函數的參數即「消息」。

例如:A類的函數調用了B類的某個函數,傳入的參數就是消息(此處與數據耦合衝突);


【數據耦合(Data coupling)】

兩個模塊間經過參數傳遞基本數據,稱爲數據耦合。 這裏有兩點須要特別關注,這也是數據耦合區別於其它耦合類型的關鍵特徵:

1)經過參數傳遞,而不是經過全局數據、配置文件、共享內存等其它方式

2)傳遞的是基本數據類型,而不是傳遞對象,例如Java中傳遞integer、double、String等類型

以下樣例中,Teacher類和Student類的耦合就是數據耦合:

Student.java

package com.oo.coupling.datacoupling;
public class Student {
/**
* 根據學號獲取學生姓名
*
@param studentId 學號,這裏就是數據耦合的地方
*
*
@return
*/

  public String getName(int studentId){

  //TODO: 查詢數據庫,獲取學生姓名,這裏演示代碼省略這部分代碼
    String name = "Bob";
    return name;
  }
  public int getRank(int studentId){

    //TODO: 查詢數據庫,獲取學生排名,這裏演示代碼省略這部分代碼
    int rank = 1;
    return rank;
  }
}

Teacher.java

package com.oo.coupling.datacoupling;
public class Teacher {
  public void printStudentRank(int studentId){ 
    Student stdu = new Student(); //Teacher類依賴Student類,經過參數傳遞類型爲int基礎數據
    studentId String name = stdu.getName(studentId);     
//Teacher類依賴Student類,經過參數傳遞類型爲int基礎數據
    studentId int rank = stdu.getRank(studentId);
    System.out.printf(" %s's rank: %d", name, rank);   } }

【數據結構耦合( Data-structured coupling)】

兩個模塊經過傳遞數據結構的方式傳遞數據,成爲數據結構耦合,又稱爲標籤耦合(Stamp coupling)。 但標籤耦合不是很好理解,並且無法和上面的「數據耦合」聯繫起來,所以

咱們通常都用數據結構耦合這個稱呼。

數據結構耦合和數據耦合是比較相近的,主要差異在於數據結構耦合中傳遞的不是基本數據,而是數據結構數據

另外須要注意的是,數據結構中的成員數據並不須要每個都用到,能夠只用到一部分。 以下樣例中,Teacher類和Student類的耦合就是數據結構耦合,且只用到了Student.id

這個成員數據:

StudentInfo.java

package com.oo.coupling.dscoupling;
public class StudentInfo { 
  public String name = "";
  public int id = 0;
  public int rank = 0;
}

Student.java

package com.oo.coupling.dscoupling;
public class Student {

    /** 
      * 獲取學生的姓名 
      * @param info 學生的信息,這裏就是data-structure coupling的地方 
      * @return 
      */ 
    public String getName(StudentInfo info){ 
        //注意:只用到了StudentInfo類的id,其它的數據成員都沒有用 
        return getNameById(info.id); 
    }
    
    /** 
      * 獲取學生排名 
      * @param info 學生的信息,這裏就是data-structure coupling的地方 
      * @return 
      */ 
    public int getRank(StudentInfo info){ 
        //注意:只用到了StudentInfo類的id,其它的數據成員都沒有用 
        return getRankById(info.id); 
    }
    
    private String getNameById(int id){ 
        String name = ""; //TODO: 查詢數據庫,獲取學生姓名,這裏演示代碼省略這部分代碼
        return name;
    }
    
    private int getRankById(int id){ 
        int rank = 0; //TODO: 查詢數據庫,獲取學生排名,這裏演示代碼省略這部分代碼
        return rank;
    }
}

Teacher.java

package com.oo.coupling.dscoupling;
public class Teacher {
    public void printStudentRank(int studentId){ 
        StudentInfo info = new StudentInfo(); 
        info.id = studentId;
        Student stdu = new Student();
        //Teacher類依賴Student類,經過參數傳遞StudentInfo數據 
        String name = stdu.getName(info);
        //Teacher類依賴Student類,經過參數傳遞StudentInfo數據 
        int rank = stdu.getRank(info);
        System.out.printf(" %s's rank: %d", name, rank);
    }
}

【控制耦合(Control coupling) 】

當一個模塊能夠經過某種方式來控制另一個模塊的行爲時,稱爲控制耦合。

最多見的控制方式是經過傳入一個控制參數來控制函數的處理流程或者輸出,例如常見的工廠類。

package com.oo.coupling.controlcoupling;
enum ToyType{Bear, Car, Gun, Bus};
public class ToyFactory {
    public String getToy(ToyType type){ 
        switch(type){ 
            case Bear: return "bear"; 
            case Car: return "car"; 
            case Gun: return "gun"; 
            case Bus: return "bus"; 
            default: return "Unknown toy"; 
        }
    }
}

【外部耦合(External coupling)】

當兩個模塊依賴相同的外部數據格式、通信協議、設備接口時,稱爲外部耦合

理解外部耦合的關鍵在於:爲何叫「外部」? 這裏的「外部」固然是與「內部」相對應的,好比前面咱們提到的各類耦合方式,能夠認爲都是「內部耦 合」,由於這些耦合都是由模塊內部

來實現的。但在外部耦合的場景下,兩個模塊內部都保持原樣,但通 過一個外部模塊(或者設備、程序、協議等)進行交互。

在軟件系統,外部依賴最典型的莫過於各類「proxy」模塊或者子系統了。好比說A系統輸出XML格式, B系統只能接收JSON格式的數據,爲了可以讓兩個系統鏈接起來,須要開發一

個轉換程序,完成格式轉 換。

【全局耦合(Globaling coupling)】

當兩個模塊共享相同的全局數據,稱爲全局耦合,又叫普通耦合(Common coupling), 不過普通耦合這個名稱太容易讓人誤解了,仍是全局耦合可以讓人顧名思義。

全局耦合是一種比較常見的耦合方式,尤爲是在C/C++的程序中,多多少少都會有一些全局數據。


【內容耦合(Content coupling)】

當一個模塊依賴另一個模塊的內部內容(主要是數據成員)時,稱爲內容耦合。內容耦合是最差的一中 耦合方式,所以有另一個很形象的名稱:病態耦合(Pathological coupling)。

高內聚低耦合

高內聚低耦合,能夠說是每一個程序猿,甚至是編過程序,或者僅僅只是在大學裏面學過計算機,都知道的一個簡單的設計原則。

雖然如此流行和人所衆知,但其實真正理解的人並很少,不少時候都是人云亦云。 要想真正理解「高內聚低耦合」,須要回答兩個問題:

1)爲何要高內聚低耦合?

2)高內聚低耦合是否意味內聚越高越好,耦合越低越好?


第一個問題:爲何要高內聚低耦合?

經典的回答是:下降複雜性。 確實很經典,固然,其實也是廢話!我相信大部分人看了後仍是不懂,什麼叫複雜性呢?

要回答這個問題,其實能夠採用逆向思惟,即:若是咱們不作到這點,將會怎樣? 首先來看內聚,試想一下,假如咱們是低內聚,狀況將會如何?


前面咱們在闡述內聚的時候提到內聚的關鍵在於「元素的凝聚力」,若是內聚性低,則說明凝聚力低;

對 於一個團隊來講,若是凝聚力低,則一個明顯的問題是「不穩定」;

對於一個模塊來講,內聚性低的問題 也是同樣的「不穩定」。

具體來講就是若是一個模塊內聚性較低,則這個模塊很容易變化。一旦變化,設 計、編碼、測試、編譯、部署的工做量就上來了,而一旦一個模塊變化,與之相關的模塊都須要跟着

改變。

舉一個簡單的例子,假設有這樣一個設計很差的類:Person,其同時具備「學生」、 「運動員」、「演員」3 個職責,有另外3個類「老師」、「教練」、「導演」依賴這個類。

Person.java

package com.oo.cohesion.low;
/** 
  * 「人」的類設計 
  * 
  */ 
public class Person {
    /** 
      * 學生的職責:學習 
      */ 
    public void study() { 
        //TODO: student's responsibility 
    }
    /** 
      * 運動員的職責:運動 
      */ 
    public void play(){ 
        //TODO: sportsman's responsibility 
    }
    /** 
      * 演員的職責:扮演
      */ 
    public void act(){ 
        //TODO: actor's responsibity 
    }
}

Teacher.java

package com.oo.cohesion.low;
/** 
  * 「老師」的類設計 
  * 
  */ 
public class Teacher {
    public void teach(Person student){ 
        student.study(); 
        //依賴Person類的「學生」相關的職責 
    }
}

Coach.java 

package com.oo.cohesion.low;
/** 
  * 「教練」的類設計 
  * 
  */ 
public class Coach {
    public void train(Person trainee){ 
        trainee.play(); 
        //依賴Person類的「運動員」職責 
    }
}

Director.java 

package com.oo.cohesion.low;
/**
 * 「導演」的類設計
 * 
 */
public class Director {
    public void direct(Person actor){ 
        actor.act(); 
        //依賴Person類「演員」的相關職責 
    }
}

在上面的樣例中,Person類就是一個典型的「低內聚」的類,很容易發生改變。好比說,如今老師要求學生也要考試,則Person類須要新增一個方法:test,以下:

package com.oo.cohesion.low;
/**
 * 「人」的類設計
 *
 */ 
 public class Person {
    /**
     * 學生的職責:學習
     */ 
    public void study() { 
        //TODO: student's responsibility 
    }
    /**
     * 學生的職責:考試
     */ 
    public void test(){ 
        //TODO: student's responsibility 
    }
    /** 
     * 運動員的職責:運動
     */ 
     public void play(){ 
        //TODO: sportsman's responsibility 
     }
    /**
     * 演員的職責:扮演
     */ 
     public void act(){ 
        //TODO: actor's responsibity 
     }
}

因爲Coach和Director類都依賴於Person類,Person類改變後,雖然這個改動和Coach、Director都沒 有關係,但Coach和Director類都須要從新編譯測試部署(即便是PHP這樣

的腳本語言,至少也要測試)。


一樣,Coach和Director也均可能增長其它對Person的要求,這樣Person類就須要同時兼顧3個類的業 務要求,且任何一個變化,Teacher、Coach、Director都須要從新編譯測試

部署。


對於耦合,咱們採用一樣的方式進行分析,即:若是高耦合,將會怎樣? 高耦合的狀況下,模塊依賴了大量的其它模塊,這樣任何一個其它依賴的模塊變化,模塊自己都須要受到

影響。因此,高耦合的問題其實也是「不穩定」,固然,這個不穩定和低內聚不徹底同樣。對於高耦合的 模塊,可能自己並不須要修改,但每次其它模塊修改,當前模塊都要編譯、測

試、部署,工做量一樣不小。

不管是「低內聚」,仍是「高耦合」,其本質都是「不穩定」,不穩定就會帶來工做量,帶來風險, 這固然不是咱們但願看到的,因此咱們應該作到「高內聚低耦合」。


回答完第一個問題後,咱們來看第二個問題:

高內聚低耦合是否意味着內聚越高越好,耦合越低越好? 按照咱們前面的解釋,內聚越高,一個類越穩定;耦合越低,一個類也很穩定,因此固然是內聚越高越好, 耦合越低越好

了。 但其實稍有經驗的同窗都會知道這個結論是錯誤的,並非內聚越高越好,耦合越低越好,真正好的設計是在高內聚和低耦合間進行平衡,也就是說高內聚和低耦合是衝突的。


對於內聚來講,最強的內聚莫過於一個類只寫一個函數,這樣內聚性絕對是最高的。但這會帶來一個明顯的問題:類的數量急劇增多,這樣就致使了其它類的耦合特別多,因而整個

設計就變成了「高內聚高耦合」 了。因爲高耦合,整個系統變更一樣很是頻繁。


同理,對於耦合來講,最弱的耦合是一個類將全部的函數都包含了,這樣類徹底不依賴其它類,耦合性是最低的。但這樣會帶來一個明顯的問題:內聚性很低,因而整個設計就變成

了「低耦合低內聚」了。因爲 低內聚,整個類的變更一樣很是頻繁。 對於「低耦合低內聚」來講,還有另一個明顯的問題:幾乎沒法被其它類重用。緣由很簡單,類自己太龐大了,

要麼實現很複雜,要麼數據很大,其它類沒法明確該如何重用這個類。

因此,內聚和耦合的兩個屬性,排列組合一下,只有「高內聚低耦合」纔是最優的設計。 所以,在實踐中咱們須要緊緊記住須要在高內聚和低耦合間進行平衡,而不能走極端。

相關文章
相關標籤/搜索