高內聚和低耦合是很原則性、很「務虛」的概念。爲了更好的討論具體技術,咱們有必要再多瞭解一些高內聚低耦合的度量標準。java
這裏先說說幾種內聚。
web
內聚數據庫
達到什麼樣的程度算高內聚?什麼樣的狀況算低內聚?wiki上有一個內聚性的分類(https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion),咱們能夠看看內聚都有哪些類型。設計模式
Coincidental cohesion:偶然內聚app
Coincidental cohesion is when parts of a module are grouped arbitrarily; the only relationship between the parts is that they have been grouped together (e.g., a 「Utilities」 class)ide
偶然內聚是指一個模塊內的各個部分是很任性地組合到一塊兒。偶然內聚的各個部分之間,除了「剛好放在同一個模塊內」以外,沒有任何關係。最典型例子就是「Utilities」類。性能
https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion
這是內聚性最弱、也是最差的一種狀況。這種狀況下,應該儘可能把這個模塊拆分紅幾個獨立模塊——即便如今不拆分,之後也早晚要拆。前陣子我就遇到了一個相似的問題。在咱們的系統中,有這樣一個處理類:
優化
public interface UserBiz UserBean queryUserBean(long userId); UserInfo queryUserInfo(long userInfoId); }
乍一看,這個接口彷佛挺「高內聚」的。可是實際上,UserBean是從本地數據庫中獲取的、記錄用戶在當前業務線中的數據的類;而UserInfo是從用戶中心獲取的、記錄用戶註冊信息數據的類:它們除了名字類似以外,基本沒有相關性。把這兩個數據的相關功能放在同一個模塊中 ,就是一種「偶然內聚」。雖然在初期的使用中,這裏並無什麼問題。可是在後續擴展時,這種「偶然內聚」致使了循環依賴,咱們不得不把它們拆分紅兩個不一樣的模塊。this
Logical cohesion:邏輯內聚spa
Logical cohesion is when parts of a module are grouped because they are logically categorized to do the same thing even though they are different by nature .
邏輯內聚是指一個模塊內的幾個組件僅僅由於「邏輯類似」而被放到了一塊兒——即便這幾個組件本質上徹底不一樣。
https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion
不少文章裏會特別指出,客戶端每次調用邏輯內聚的模塊時,須要給這個模塊傳遞一個參數來肯定該模塊應完成哪種功能 。這是由於邏輯內聚的幾個組件之間並無什麼本質上的類似之處,於是從入參提供的業務數據中沒法判斷應該按哪一種邏輯處理,而只好要求調用方額外傳入一個參數來指定要使用哪一種邏輯。
我早期作「可擴展」的設計時,常常會產生這種內聚。例如,有一個計算還款計劃的接口,我是這樣設計的:
public interface RepayPlanCalculator{ List<RepayPlan> calculate(LendApply apply, CalculateParam param, CalculateMethond calculateMethod);}
除了借款申請和必要的計算參數(本金、期數、利率等)以外,這個接口還要求調用方傳入一個計息方式字段,用以決定是使用等額本息、等額本金仍是其它公式計算利息。若是某天要增長一種計息方式,好比先息後本,也很好辦:增長一種CalculateMethond就行。
看起來一切都好,直到有一天業務要求停用等額本金方式,統一採用等額本息方式計算還款計劃表。這時候咱們只有兩種選擇:要麼讓全部的調用方排查一遍本身調用這個接口時傳入的參數,保證入參calculateMethod只傳入了等額本息方式;或者,在接口內部作一個轉換:調用方傳入了等額本金方式,那麼按等額本息方式處理。顯然,第一種方式會把本來很小的一個需求變化擴散到整個系統中。這就好像只是被蚊子盯了一口卻全身都長了大包同樣。若是某一個調用方改漏了,那麼它獲得的還款計劃表就是錯的。若是這份錯誤的還款計劃表到了用戶手裏,那麼投訴扯皮事故覆盤就少不了了。第二種方式則容易讓調用方產生誤解——明明指定了等額本金方式,爲何計算結果是等額本息的?這就比如下單點了一份蝦滑上菜給了一份黃瓜。若是這種誤解一路傳遞給了用戶——例如某個調用方的開發、產品一看參數支持等額本金,因而向用戶宣傳「咱們的產品支持等額本金」——那麼投訴扯皮事故覆盤就又要出現了。
邏輯內聚也是一種「低內聚」,它把接口內部的邏輯處理暴露給了接口以外。這樣,當這部分邏輯發生變動時,本來無辜的調用方就要受到牽連了。
Temporal cohesion:時間內聚
Temporal cohesion is when parts of a module are grouped by when they are processed - the parts are processed at a particular time in program execution
時間內聚是指一個模塊內的多個組件除了要在程序執行到同一個時間點時作處理以外、沒有其它關係。
https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion
概念有點晦澀,舉個例子就簡單了:當Controller處理Http請求以前,用Filter統一作解密、驗籤、認證、鑑權、接口日誌、異常處理等操做,那麼,解密/驗籤/認證/鑑權/接口日誌/異常吹了這些功能之間就產生了時間內聚。這些功能之間本來沒有什麼關係,可是考慮到這種時間內聚,咱們通常會把它們放到同一個包下、或者繼承同一個父類。
/*** 入參解密*/class DecodeFilter extends HttpFilter{ // 略}/*** 入參驗籤*/class SignFilter extends HttpFilter{ // 略}/*** 登陸認證*/class CertificationFilter extends HttpFilter{ // 略}// 其它相似,略
這些操做、功能之間並無必然的聯繫——從這一點上來看,時間內聚也是一種弱內聚。但它多少仍是比偶然內聚和邏輯內聚要更強一些的:畢竟它們聚在一塊兒是有正當理由的。就比如哪怕你都叫不全大學同班同窗的名字,但畢業十週年的時候聚一聚也是合情合理的。
Procedural cohesion:過程內聚
Procedural cohesion is when parts of a module are grouped because they always follow a certain sequence of execution.
過程內聚是指一個模塊內的多個組件之間必須遵循必定的執行順序才能完成一個完整功能。
https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion
顯然,過程內聚已是一種比較強的內聚了。存在過程內聚的幾個功能組件應該儘量地放在一個模塊內,不然在後續的維護、擴展中必定要吃苦頭。
在前面提到的那個金額計算的模塊中,存在下面這種狀況:
/** 計算器基類 */public abstract class Calculator{ private String formula; protected Calculator(String formula){ super(); this.formula=fomrula; } public abstract CalculateResult calculate(CalculateParam money);}
/** 分期服務費計算器 */public class InstallmentServiceFeeCalculator exstends Calculator{ public ServiceFeeCalculator(){ // 分期服務費公式:分期本金*服務費費率 super("installmentPrincipal*serviceFeeRate"); } /** 計算分期服務費 */ CalculateResult calculate(CalculateParam money){ // 注意:這裏必須保證已經調用過InstallmentPricipalCalculator // 並已經計算出了分期本金 }}/** 分期本金計算器 */public class InstallmentPricipalCalculator extends Calculator{ // 略}
InstallmentServiceFeeCalculator是用來計算分期服務費的一個類。從分期服務費的計算公式能夠看出:在計算分期服務費以前,必須先計算出分期本金。這樣,InstallmentServiceFeeCalculator與InstallmentPricipalCalculator之間就有了過程耦合。應對這種狀況,咱們有兩種選擇:一是讓調用方在計算分期服務費以前,先本身計算一遍分期本金,而後把計算結果傳給分期服務費計算器;二是讓分期服務費計算器在必要的時候本身調用一次分期本金計算器。
顯然,第二種方式比第一種更好:分期服務費計算器和分期本金計算器之間存在過程耦合,第二種方式把它們放到了同一個模塊內部。這樣,不管哪一個計算器發生變化——修改公式、變動取值來源等——均可以只修改這個模塊,而不會影響到調用方。
Communicational/informational cohesion:通訊內聚
Communicational cohesion is when parts of a module are grouped because they operate on the same data (e.g., a module which operates on the same record of information).
通訊內聚是指一個模塊內的幾個組件要操做同一個數據(例如同一個Dto、同一個文件、或者同一張表等)。
https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion
對設計模式熟悉的同窗必定不會對通訊內聚感到陌生:責任鏈/代理等模式就是很典型的通訊內聚。例如,咱們曾有一個模塊應該是這樣的:
public interface DataCollector{ void collect(Data data);}class DataCollectorAsChain implements DataCollector{ private List<DataCollector> chain; @Override public void collect(Data data){ chain.foreach(collector-> collector.collect(data)); }}class DataCollectorFromServerA implements DataCollector{ @Override public void collect(Data data){ // 從數據庫裏查到一堆數據 data.setDataA(xxx); }}// 此外還有相似的從ServerB/ServerC的接口獲取數據的幾個類;// 這些類最終都會組合到DataCollectorAsChain的chain裏面去。
上面是一個典型的責任鏈模式。責任鏈上每一環都須要向Data中寫入一部分數據,最終獲得一個完整的Data。很顯然,DataCollectorFromDb和DataCollectorFromRpc、DataCollectorFromHttp之間存在着通訊內聚,它們應該被放到同一個模塊內。
然而在咱們的系統中,這一條完整的責任鏈被完全拆散,零零碎碎地分佈在業務流程的各個角落裏;有些字段甚至被分散在了分佈部署的好幾個服務上。因而乎,咱們要查找某個字段取值問題時,總要翻遍整個流程才能肯定它到底在哪兒賦值、要如何修改;若是要增長字段、或者修改某些字段的數據來源,甚至要修改好幾個系統的代碼。這就是打破通訊內聚形成的惡果。
Sequential cohesion:順序內聚
Sequential cohesion is when parts of a module are grouped because the output from one part is the input to another part like an assembly line.
順序內聚是指在一個模塊內的多個組件之間存在「一個組件的輸出是下一個組件的輸入」這種「流水線」的關係。
https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion
若是熟悉Java8的Lambda表達式的話,應該很容易想到:Java8中的Stream就是一個順序內聚的模塊。例以下面這段代碼中,從bankcCardList.stream()開啓一個Stream以後,filter/map/map每一步操做的輸出都是下一個操做的輸入,並且它們必須按順序執行,這正是標準的順序內聚:
List<BankCard> bankCardList = ...;User u = ...;String bankCardPhone = bankcCardList.stream() .filter(card->card.no().equals(u.getBankCardNo()))) .map(BankCard::getPhone()) .map(phone -> "*******" + phone.subString(phone.lengh()-4))) .orElse(StringUtils.EMPTY);
除了Stream以外,設計模式中的裝飾者/模板/適配器等模式也是很典型的順序內聚……等等。例如,咱們來看這段代碼:
public interface FlwoQueryService{ Optional<Flow> queryFlow(Queryer queryer);}class FlwoQueryServiceFromDbImpl{ public Optional<Flow> queryFlow(Queryer queryer){ // 從數據庫裏查詢用戶流程,略 }}abstract class FlowQueryServiceAsDecorator implements FlowQueryService{ private FlwoQueryService decorated; public Optional<Flow> queryFlow(Queryer queryer){ // 裝飾者,在decorated查詢結果的基礎上,作一次裝飾處理 return decorated(queryer).map(flow-> decorate(flow, queryer)); } /** 加強方法 */ protected abstract Flow decorate(Flow flow, Queryer queryer);}class FlowQueryServiceNotNullImpl extends FlowQueryServiceAsDecorator{ protected Flow decorate(Flow flow, Queryer queryer){ // 若是flow爲null,則建立一個新數據 }}
在上面的裝飾者——固然也能夠叫模板——類中,這兩個步驟的順序是固定的:必須先由被裝飾者執行基礎的查詢操做、再由裝飾者作一次加強操做;並且被裝飾者的查詢結果也偏偏就是裝飾操做的一個入參。能夠說,這段代碼很完美的解釋了什麼叫「順序內聚」。
這段代碼是咱們重構優化後的成果。在重構以前,咱們只有FlwoQueryServiceFromDbImpl。調用方須要本身判斷和處理數據庫中沒有數據的狀況,加上不一樣業務場景下對沒有數據的處理方式不一樣,類似但不徹底相同的代碼重複出現了好幾回。所以,當處理邏輯發生變化——例如庫表結構變了、或者字段取值邏輯變了時——咱們須要把全部引用的地方都檢查一遍、而後再修改好幾處代碼。而在重構以後,全部處理邏輯都集中到了這個裝飾者模塊內,咱們能夠很輕鬆地肯定影響範圍、而後統一地修改代碼。
Functional cohesion (best):功能內聚(最強內聚)
Functional cohesion is when parts of a module are grouped because they all contribute to a single well-defined task of the module .
功能內聚是指一個模塊內全部組件共同完成一個功能、缺一不可。
https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion
功能內聚是最強的一種內聚。其它內聚更多的是在討論把哪些組件組合成一個模塊;而功能內聚的意義在於:它討論的是把哪些組件提出當前模塊。即便某個組件與模塊內組件存在順序內聚、通訊內聚、過程內聚,但只要這個組件與這個模塊的功能無關,那這個組件就應該另謀高就。
例如,咱們系統中有一個調用規則引擎的模塊:
public interface CallRuleService{ RuleResult callRule(RuleData data);}class CallRuleService implements CallRuleService{ public RuleResult callRule(RuleData data){ validate(data); RuleRequest request = transToRequest(data); RuleResponse response = callRuleEngin(request); return transToResult(response); }}
不管是校驗、構建請求、調用引擎仍是解析結果,這個模塊中全部的代碼都是爲了實現一個功能:調用規則引擎並解析結果。可是,隨着業務發展、需求變動,這個模塊中出現了愈來愈多的「噪音」:把調用規則引擎的request和response入庫、在封裝數據時把某個數據同步給某個系統、在獲得響應後把某個字段發送給另外一個系統……諸如此類,不一而足。這些業務需求並不直接與「調用規則引擎」這個核心功能,相關組件與「調用核心規則」也只是順序內聚(須要使用調用規則引擎的返回結果)、通訊內聚(須要使用調用規則引擎的入參/出參)甚至只是時間內聚(須要在調用規則引擎時同步數據)。從「功能內聚」的角度來看,這些新增代碼就不該該放到這個模塊中來。
可是,因爲一些歷史緣由,這些代碼、組件、需求全都被塞到了這個模塊中。結果,這個模塊不只代碼很是臃腫,並且性能也十分低下:一次用戶請求經常要20多秒才能完成,但是因爲模塊可維護和可擴展性差,重構優化也很是困難。若是當初能遵循「功能內聚」的要求,把沒必要要的功能放到別的模塊下,咱們也不會像如今這樣望洋興嘆、無從下手了。
練習
我在《高內聚與低耦合》文中舉過一個這樣的例子:
這個模塊中的組件屬於哪一種內聚呢?
嚴格一點說,右側那些組件——從「提交信息」到「發送短信驗證碼」或「判斷短信驗證碼是否正確」——屬於功能內聚。它們全都是爲了完成「短信簽約」這個操做而組合到當前模塊下的。
可是,左側這些組件——從「後續業務分發器」到「後續業務處理A」等——之間,只能算時間內聚。各類後續業務處理之間並無直接的、或者本質上的關聯,它們被放在這個模塊中的緣由僅僅是他們都要在短信簽約完成以後作一些處理。這能夠說是標準的時間內聚。
左側和右側組件之間呢?從上面的分析也能看出來:這兩大部分之間是順序內聚。這個模塊必須先調用右側組件,在它們處理完成後才能去調用左側組件進行處理。
在《抽象》一文中,還有這樣一個例子:
public interface CardListService{ List<Card> query(long userId, Scene scene);}//核心實現是這樣的public class CardListServiceImpl{ private Map<Scene, CardListService> serviceMap; public List<Card> query(long userId, Scene scene){ return serviceMap.get(scene).query(userId, scene); }}// 返回字段是這樣的public class Card{ // 客戶端根據這個字段的值來判斷當前銀行卡是展現仍是置灰 private boolean enabled; // 其它卡號、銀行名等字段,和accessor略去}// 入參是這樣的public enum Scene{ DEDUCT, UN_BIND, BIND;}
在這個組件中,用於處理DEDUCT/UN_BIND/BIND等各類邏輯的組件之間是什麼內聚關係呢?我認爲是通訊內聚:它們都要針對入參userId和scene作處理,並返回一樣的List<Card>。