代碼耦合是怎麼回事呢?
耦合,誰之錯?業務耦合,架構耦合,代碼耦合,依次產生,前者是後者的催化劑,最終結果是系統嚴重耦合,無法適應任何變化。
這其中,業務耦合是根本,必須從根防治與修正,否則沒有用,只會越來越差,最終崩塌。當然,耦合也要從業務、架構、代碼三個層面抓起,在每個層面減少耦合,為後面減少耦合打好基礎。
以前,我寫代碼時,我考慮模塊(本文中的模塊就是指單個源文件)的單向依賴關係,考慮介面的正交性和緊湊性。
我覺得我在做低耦合的好設計。然而,我發現其他程序員寫的代碼依賴關係混亂,介面臃腫,但他們仍然覺得自己寫的代碼耦合很低,設計很好。
我這才發現,我理解的耦合和他們理解的不一樣。他們理解的低耦合就是把代碼提出來,讓代碼不要「亂」。然而,對於什麼是「耦合」、什麼是「亂」,他們並不知道有什麼客觀標準可以度量。所以,現在我相信「耦合」是個有歧義的壞詞,「低耦合」是個程序員經常誤用的理論。
我建議,設計架構、考察模塊之間關係時,不要用「耦合」、「亂」這些無法度量的詞語,而應該改用以下三個可以度量的指標:依賴、正交性、緊湊性。依賴和耦合的最大區別在於,當我們說「A和B耦合」時,在字面含義中,A和B二者平等。
然而,正確的模塊關係根本不應該平等,而應該是單向依賴才對。所以我們應該說「A依賴B」,這樣含義要清楚得多。A依賴B意味著,A模塊可以調用B模塊暴露的API,但B模塊絕不允許調用A模塊的API。單向依賴是紅線,好的設計一定不會違反這條紅線。
注意:根據實質重於形式原則,本文中的「依賴」指人腦中的依賴而不是編譯器的依賴。
只要程序員編寫模塊A時,需要知道模塊B的存在,需要知道模塊B提供哪些功能,A對B依賴就存在。甚至就算通過所謂的依賴注入、命名查找之類的「解耦」手段,讓模塊A不需要import B或者include "B.h",人腦中的依賴仍舊一點都沒有變化。唯一的作用是會騙過後文會提到的代碼打分工具,讓工具誤以為兩個模塊間沒有依賴。所以 @翟志軍 的例子中的解耦只是在自娛自樂兜圈子而已。
因為不管你用什麼技術手段,Configuration的調用方程序員總是需要知道配置項的具體業務含義才能用啊。很多程序員覺得「耦合」是壞事,愛寫兜圈子的代碼來消除所謂的「耦合」。
但是我們改用「依賴」術語後,我們就發現單向依賴不是壞事,根本不必費心消除,因為,真正的重點是,依賴關係中,被依賴方暴露的介面能不能足夠「正交」和「緊湊」。正交性是指一個模塊提供的API中,多個方法之間是否有重複的功能。
如果有重複功能,正交性就差。通常,正交性高的模塊更穩定,不會因為上層業務變化而被迫修改代碼。好的API內部的多個方法之間不應該有任何重複功能,只實現正交的機制。如果感覺拆得太細使用不便,應該在底層API之外包裝出一層Helper、Utility組成的膠水層。
膠水層調用底層原語API來實現常用模式供上層使用。對於膠水層中的模塊,對正交性的要求可以稍低一些。注意上層代碼既可以直接調用正交的底層API,又可以調用膠水層的常用模式。緊湊性是指一個模塊提供的API中,公有方法總數必須很少,每個方法的參數也必須很少。
《Unix編程藝術》上說一個模塊不要超過7個方法,不然就很難理解。但我實踐中發現,我一般編寫的模塊,公有方法通常不超過3個。
總之,單向依賴、正交性、緊湊性這三個指標都很務實,有客觀方法可以度量。
我覺得也許可以代替「低耦合」這種「公說公有理婆說婆有理」的務虛理論。比如前兩天我們ThoughtWorks的諮詢師郵件列表中就有人分享一些工具,用上述標準自動檢查代碼質量。那麼將來你和別的程序員約架,你要噴對方代碼寫得爛,你就有了依據,因為你可以直接用工具給代碼打分。另外,將來如果有同事或者網友在討論編程時,用了「耦合」、「解耦」、「亂」之類的混亂術語,你可以把這篇文章發給他看,然後鄙視他。可以看看這篇博客:耦合的本質 - 翟志軍
代碼耦合的本質是一方對另一方的假設。兩方之間的假設越多,兩方的耦合度就越高。當然現實中,往往會遇到多方耦合。
舉例子:
Java開發的A系統與B系統進行RPC調用:這裡的假設是雙方都使用相同的語言Java,同時要求是相互兼容的Java版本。如果A系統想換成Ruby開發,那麼假設不成立,所以,A系統和B系統在語言層次上是耦合的。所以,現在很多系統用Http,就可以解耦了,因為大家假設對方使用的是Http協議,而不是具體實現舉個更具體的例子:
我們設計一個配置類時有兩種方式:方式一:public class Configuration {
private String propertiesPath;
public Configuration(String propertiesPath) {
this.propertiesPath = propertiesPath;
}
public String read(String key){
return FileUtils.readProperties(propertiesPath).read(key)
}
}
方式二:
public interface Configuration {
String read(String key);
}
public class FileConfiguration extends Configuration {
private String propertiesPath;
public Configuration(String propertiesPath) {
this.propertiesPath = propertiesPath;
}
public String read(String key){
return FileUtils.readProperties(propertiesPath).read(key)
}
}
方式一的假設是我們的配置都是寫在properties文件里的,而且是放在文件系統中的。這就意味著你的配置是與properties文件及文件系統耦合的。
方式二就沒有這樣的假設了。因為Configuration只是一個介面,它的假設是配置都可以通過一個key參數,然後返回一個String的value。它並沒有假設你的配置到底從哪裡來。這樣就實現了解耦。耦合就是你和另一個小年輕合租。
就算你們訂的計劃再好再完美再沒有交集,至少你早上起床拉屎的時候,都會和對方有衝突。
追求低耦合是有必要的,也是高效的,但是一味追求降低耦合,到一定程度,自然做的全是無用功推薦一本書《浮現式設計》,其中對耦合的概念闡述的比較好,該著作將耦合劃分為四個類型,我整理一下,並畫了4個圖供讀者理解:
- 標示耦合實體A 知道 實體B的存在,但不知道實體B怎麼使用(即不會調用實體B的任何方法) 在這種耦合情況下,實體B如果被刪除掉,會影響實體A的編譯,但是修改實體B的方法或實現,對實體A沒有影響。 標示耦合是最基本的耦合,大量存在系統中。 可以通過組合的方式,比如實體有變數包含實體B或者實體B的集合。 實際場景:訂單子項表中關聯產品實體表,表示該訂單子項對應相應的商品。
- 表示耦合實體A 除了知道實體B存在,還調用了實體B的方法。 在這種情況下,實體B的介面如果修改,比如更名、增減參數,會影響實體A的編譯。這時就是表示耦合。表示耦合比較常見,好壞視同實際場景。 實際場景:訂單子項表調用了ProductPo的getPrice(獲取價格)來計算該項目的總價。
- 子類耦合實體A 調用 介面B 的方法來完成某項工作,實體A 的調用範圍限定在介面B提供的方法,至於它的子類怎麼實現的實體A不關心(應該是第三方工廠類C關注的),但是實體A 對介面B的實例進行強制轉型,轉為子類D,這樣實體A就可以調用子類D的非介面方法,那實體A就和子類D產生了耦合。子類耦合是不好的耦合。 實際場景:訂單計算促銷規則時,直接調用規則介面的實現類計算。
- 繼承耦合實體A 繼承實體B,通過繼承獲得實體B的保護級別或以上的方法。
繼承耦合一般情況下是好的耦合,有利於減少冗餘,但是但繼承深度不斷變化時,會出現一些父類方法並不是子類需要的,而且如果使用了模板模式方法時,子類還被迫實現自己不需要的抽象方法(UnsupportedMethodException),那就成了不好的耦合了。
使用場景,一般在框架內,比如一些公共方法(增刪改查)可以在抽象類中實現。
輕視非功能性需求導致職責劃分不夠清楚,或者不夠細。
在這基礎上怎麼做都有強耦合。至於假設,這是比較高級且比較難發現的,也是帶來耦合的一個重要部分。但以我的經驗,大部分項目還沒到因假設而耦合的地步就爛掉了。剔除耦合的確是正確的方向,但是為了剔除耦合而剔除耦合,是否過於執著了? 耦合也要看個權重吧?
業務耦合是正常的,架構耦合代碼耦合就是時間給的少或者人水平低
推薦閱讀:
※能否比較一下常見C++開發框架的API風格?
※Web API介面如何防止本網站/APP以外的調用?
※如何才能寫出簡潔好看的API文檔,有沒有開源框架可以用?
※SDK和API的區別?
※網站後台要做客戶端API介面,介面文檔如何寫?