避免過早的軟體抽象 - Jonas

banq發表於2021-11-19

讓我們看一些在實踐中經常發生的過早抽象的具體案例。這些都是基於在我們自己的程式碼庫中找到的真實示例。
  1. 職責抽象得太細了
  2. 使用設計模式沒有真正的好處
  3. 效能過早最佳化
  4. 低耦合無處不在

讓我們分別仔細看看其中的每一個。
 

1.職責抽象得太細了
複雜程式碼庫的一個根本原因是職責在過於精細的級別上劃分。這可能是將資料庫查詢抽象到專用儲存庫類中,將 HTTP 呼叫抽象到服務類中,或者將某些完全內部的邏輯移到單獨的元件中。
這樣做通常是為了滿足高度流行的單一職責的 SOLID 原則——每個類應該只有一個改變的理由,或者,他們應該只有一份工作。如果我們把每一小塊邏輯拆分成一個單獨的類,那麼一切都有非常明確的職責,只做一個工作,因此只有一個改變的理由。很棒吧?問題是所有這些小部分通常仍然緊密耦合並高度依賴於彼此。如果各個部分之間的任何通訊發生變化,通常會產生級聯效應,需要對其中的許多部分進行更改。所以他們每個人可能只有一個改變的理由,但是如果一個單一的改變經常需要對許多部分進行改變,那麼改變程式碼就很痛苦,那也沒有好處。
此外,僅僅因為一個原因而改變類通常沒有真正的實際優勢。事實上,在做不止一件事的類中進行更改通常為開發人員提供了更多的上下文,這使得理解更改及其對周圍程式碼的影響變得更加容易。
那麼我們什麼時候應該分擔責任呢?一種常見且非常有效的情況是需要在多個地方使用邏輯。如果在程式碼中的多個位置需要完全相同的 HTTP 呼叫或資料庫查詢,重複邏輯通常會降低可維護性。在這種情況下,將其移至共享且可重用的元件可能是一個好主意。關鍵是在需要之前不要這樣做。另一個有效的情況是當邏輯非常複雜並對周圍程式碼的可讀性產生負面影響時。如果一段邏輯佔用 300 行程式碼,這可能是這種情況,而僅用幾行程式碼可能最終只會損害可讀性並使程式碼導航變得更加困難。請記住,拆分職責總是會給程式碼增加更多的結構複雜性。
下面你會看到改變我們對類職責的看法如何影響帖子頂部顯示的原始架構。在左側,我們將來自服務類的邏輯直接放入需要服務邏輯的命令處理程式中。在右側,我們將儲存庫類中的資料庫查詢直接移動到需要它的事件處理程式中。

避免過早的軟體抽象 - Jonas
在左側,我們將來自 Service 類的邏輯直接放入需要它的命令處理程式中。在右側,我們將一個資料庫查詢移動到一個 Repository,直接進入需要它的 Event Handler。
 

2. 使用的設計模式沒有真正的好處
在真正需要它們的好處之前引入各種程式設計設計模式是另一個常見的陷阱。設計模式非常擅長解決程式碼庫中的特定問題,並且在某些情況下可以降低整體複雜性。也就是說,幾乎所有這些都帶有增加結構複雜性和降低程式碼一致性的缺點。
一個很好的例子是裝飾模式。裝飾器模式通常用於在現有元件之上新增附加功能。這可能是一個發出 HTTP 請求的元件,我們希望向其新增重試機制。在不改變原有元件的情況下,我們可以新增一個新的元件來包裹原有的元件,並在上面新增重試邏輯。透過實現相同的介面,它可以直接在程式碼中或透過依賴注入替換原始元件。
乍一看,這似乎是個好主意。我們不必更改任何現有程式碼,我們可以單獨測試它們中的每一個,並且在單獨檢視時每個部分都很容易理解。巨大的不利之處在於我們再次失去連貫性。當開發人員稍後檢視原始元件或使用該元件的程式碼時,不會立即清楚執行程式碼時會發生什麼,因為在“幕後”頂部新增了更多邏輯。我見過將重試直接新增到類的真實案例,但後來發現它已經用重試邏輯裝飾,最終在部署時重試多次。像這樣的情況只有在不清楚程式碼的行為方式時才會真正發生。
另一種廣泛使用的模式是命令釋出-訂閱模式. 這裡的一個類,不是直接處理請求,而是抽象成一個命令在別處處理。一個示例可能是 API 控制器將 HTTP 請求對映到命令,並將它們釋出以由訂閱此特定命令的適當處理程式處理。這在接收和解釋請求的程式碼部分與知道如何處理請求的部分之間提供了鬆散耦合和清晰的隔離。這種模式存在合法的用例,但在實踐中需要質疑其是否只是一個無用的對映層,對映層進一步使跟蹤程式的執行路徑變得更加困難,因為釋出者不知道命令最終在哪裡被處理。
這些只是經常過早使用的設計模式的幾個例子。幾乎所有的模式都可以這樣說。它們都有缺點,所以只有在需要好處並超過缺點時才使用模式。
下面您將再次看到刪除不必要的設計模式對我們原始架構的影響。在左邊,我們刪除了裝飾器模式,在右邊,包括髮布/訂閱機制在內的整個命令流都被刪除了。

避免過早的軟體抽象 - Jonas
刪除過早引入的設計模式。左側的裝飾模式已移除。刪除了右側的命令和釋出/訂閱模式。
 

3.效能最佳化過早
構建效能良好的軟體至關重要,通常最有效的問題解決方案是最乾淨、最簡單的。但在其他時候,情況並非如此。在這裡,最佳化的成本必須與我們期望獲得的實際實際收益相對。需要考慮的成本包括分析、實施和維護最佳化所花費的時間,以及使用更復雜的方法來提高效率可能會降低程式碼可讀性。不要為了不必要的效率而犧牲程式碼可讀性,並記住開發人員的時間成本通常遠遠超過透過微最佳化程式碼節省計算資源的潛在收益。

過早的最佳化是萬惡之源——Donald Knuth
也可以在架構級別進行最佳化。一個示例是命令查詢職責分離 (CQRS)模式。CQRS 本質上意味著您有兩個獨立的資料模型,一個用於更新資料,另一個用於讀取資料,將您的應用程式分為讀寫端。這允許最佳化一側以實現高效讀取,並最佳化另一側以實現高效寫入,並且在您的應用程式特別重讀或重寫的情況下,可以將一側比另一側擴充套件更多。
這種模式的巨大缺點是需要構建和維護一個完整的獨立資料模型,從而導致大量的開發開銷。如果需要效能,這種權衡可能很好,但即使對於數百萬人使用的應用程式,我也很少看到增加的讀取或寫入效率提供任何可衡量的好處。更明智的方法是對讀取和寫入使用單一模型,並且僅針對已知簡單方法無法充分執行的少數個別情況建立最佳化的讀取模型。
下面您會看到從我們的示例流程中刪除讀取端的插圖。後續查詢不會從專用的讀取模型表中讀取,而是直接從事件源中讀取,在我們的示例中,事件源是資料最初寫入的位置。

避免過早的軟體抽象 - Jonas
刪除應用程式的整個專用讀取端,轉而使用相同的模型進行讀取和寫入。
 

4. 低耦合介面太多
低耦合的程式碼庫是每個部分都儘可能獨立於其他部分的程式碼庫。低耦合使得對一個部分的更改對其他部分的影響很小,並且可以更輕鬆地更改一部分程式碼,因為它們僅以最少的方式相互依賴。
實現低耦合的典型方法是遵循依賴倒置開放/封閉SOLID 原則,即實體應該依賴抽象而不是具體實現,同時對擴充套件開放,對修改關閉。在實踐中,這通常是透過抽象介面後面的類並讓其他類依賴於這些介面而不是具體類來完成的。
當對這些原則的解釋導致低耦合被引入到任何地方時,問題就出現了:

低耦合,特別是介面,使程式碼不那麼連貫,更難導航,因為您不直接知道將執行哪些具體程式碼。
您首先需要檢查介面存在哪些實現,然後找出在執行時實際使用的是哪個實現。此外,介面是另一個需要新增到專案中的檔案,並且在具體實現的簽名發生變化時保持最新。
介面解決了大量問題,在需要解決實際實際問題時引入它們,而不是為了實現不必要的低耦合而提前引入。通常,這將是當您需要能夠更換實現時,或者在建立其他人使用的外部庫而無法修改庫程式碼庫時。此外,如果您只是使用介面來允許在測試中進行模擬,請認真考慮切換到允許模擬具體類的模擬庫以避免開銷。
下面透過刪除兩個介面進行視覺化,讓事件處理程式和命令處理程式分別直接引用儲存庫和服務類的具體實現。

避免過早的軟體抽象 - Jonas
刪除不需要的介面。左側是儲存庫的介面,右側是服務類的介面。

相關文章