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


內聚

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

 

在面向對象領域,談到“內聚”的時候,模塊的概念遠遠不止我們通常所理解的“系統內的某個模塊”這個范圍,而是可大可小,大到一個子系統,小到一個函數,你都可以理解為內聚

里所說的 “模塊”。所以可以用“內聚”來判斷一個函數設計是否合理,一個類設計是否合理,一個接口設計是否合理, 一個包設計是否合理,一個模塊/子系統設計是否合理。

其次:“元素”究竟是什么? 有了前面對“模塊”的深入研究后,元素的含義就比較容易明確了(不同語言稍有不同)。

函數:函數的元素就是“代碼”

類/接口:類的元素是“函數、屬性”

包:包的元素是“類、接口、全局數據”等

模塊:模塊的元素是“包、命名空間”

 

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

想到的肯定是“你中有我、我中有你”這樣 的含義,甚至可能會聯想到“美女和帥哥”的結合,抑或“青蛙王子和公主”的結合這種情況。這樣的理解本身也並沒有錯,但比較狹隘。 我們

以類的設計為例:假如一個類里面的函數都是只依賴本類其它函數(當然不能循環調用啦),那內聚性肯定是最好的,因為“結合”得很緊密

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

 

【內聚的分類】 

以下各種形式的內聚的內聚性越來越高

【巧合內聚(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都需要重新編譯測試

部署。


對於耦合,我們采用同樣的方式進行分析,即:如果高耦合,將會怎樣? 高耦合的情況下,模塊依賴了大量的其它模塊,這樣任何一個其它依賴的模塊變化,模塊本身都需要受到

影響。所以,高耦合的問題其實也是“不穩定”,當然,這個不穩定和低內聚不完全一樣。對於高耦合的 模塊,可能本身並不需要修改,但每次其它模塊修改,當前模塊都要編譯、測

試、部署,工作量同樣不小。

無論是“低內聚”,還是“高耦合”,其本質都是“不穩定”,不穩定就會帶來工作量,帶來風險, 這當然不是我們希望看到的,所以我們應該做到“高內聚低耦合”。


回答完第一個問題后,我們來看第二個問題:

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

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


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

設計就變成了“高內聚高耦合” 了。由於高耦合,整個系統變動同樣非常頻繁。


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

了“低耦合低內聚”了。由於 低內聚,整個類的變動同樣非常頻繁。 對於“低耦合低內聚”來說,還有另外一個明顯的問題:幾乎無法被其它類重用。原因很簡單,類本身太龐大了,

要么實現很復雜,要么數據很大,其它類無法明確該如何重用這個類。


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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM