創建沒有依賴關係的真正模塊化代碼

已發表: 2022-03-11

開發軟件很棒,但是……我想我們都同意這可能有點像過山車。 一開始,一切都很好。 您可以在幾天甚至幾小時內一個接一個地添加新功能。 你在滾!

快進幾個月,你的開發速度就會下降。 是因為沒有以前那麼努力了嗎? 並不真地。 讓我們再快進幾個月,你的開發速度進一步下降。 在這個項目上工作不再有趣,已經成為一種拖累。

它變得更糟。 您開始在應用程序中發現多個錯誤。 通常,解決一個錯誤會產生兩個新錯誤。 此時,您可以開始唱歌:

代碼中的 99 個小錯誤。 99 個小蟲子。 取下一個,修補它,

…代碼中有 127 個小錯誤。

你現在對這個項目的工作感覺如何? 如果你像我一樣,你可能會開始失去動力。 開發此應用程序只是一種痛苦,因為對現有代碼的每次更改都可能產生不可預知的後果。

這種經歷在軟件世界中很常見,可以解釋為什麼這麼多程序員想要扔掉他們的源代碼並重寫一切。

軟件開發隨著時間的推移而放緩的原因

那麼這個問題的原因是什麼?

主要原因是複雜性上升。 根據我的經驗,造成整體複雜性的最大因素是,在絕大多數軟件項目中,一切都是相互關聯的。 由於每個類都有依賴關係,如果您更改發送電子郵件的類中的某些代碼,您的用戶突然無法註冊。 這是為什麼? 因為您的註冊碼取決於發送電子郵件的代碼。 現在你不能在不引入錯誤的情況下改變任何東西。 根本不可能跟踪所有依賴項。

所以你有它; 我們問題的真正原因是來自我們代碼所具有的所有依賴項的複雜性。

大泥球及其減少方法

有趣的是,這個問題已經知道多年了。 這是一種常見的反模式,稱為“大泥球”。 我多年來在多家不同公司從事的幾乎所有項目中都看到了這種類型的架構。

那麼這個反模式到底是什麼? 簡單地說,當每個元素都與其他元素有依賴關係時,你會得到一個大泥球。 下面,您可以看到著名開源項目 Apache Hadoop 的依賴關係圖。 為了可視化大泥球(或者更確切地說,大毛線球),您繪製一個圓圈並將項目中的類均勻地放置在其上。 只需在每對相互依賴的類之間畫一條線。 現在您可以看到問題的根源。

Apache Hadoop 的“大泥球”的可視化,有幾十個節點和數百條將它們相互連接的線。

Apache Hadoop 的“大泥球”

模塊化代碼的解決方案

於是我問了自己一個問題:有沒有可能在降低複雜度的同時還能像項目開始時一樣享受樂趣? 說實話,你無法消除所有的複雜性。 如果你想添加新特性,你總是不得不提高代碼的複雜性。 然而,複雜性可以被移動和分離。

其他行業如何解決這個問題

想想機械行業。 一些小型機械廠在製造機器時,他們會購買一套標準元件,製造一些定制的,然後將它們組合在一起。 他們可以完全獨立地製造這些組件並在最後組裝所有東西,只需進行一些調整。 這怎麼可能? 他們通過設定的行業標準(如螺栓尺寸)和預先決定(如安裝孔的尺寸和它們之間的距離)了解每個元素如何組合在一起。

物理機制的技術圖以及它的各個部分如何組合在一起。這些部分按接下來要附加的順序編號,但從左到右的順序是 5、3、4、1、2。

上述組件中的每個元素都可以由對最終產品或其其他部件一無所知的獨立公司提供。 只要每個模塊化元件都按照規格製​​造,您就可以按計劃創建最終設備。

我們可以在軟件行業複製嗎?

我們當然可以! 通過使用接口和反轉控制原理; 最好的部分是這種方法可以在任何面向對象的語言中使用:Java、C#、Swift、TypeScript、JavaScript、PHP——這樣的例子不勝枚舉。 您不需要任何花哨的框架來應用此方法。 您只需要遵守一些簡單的規則並保持自律。

控制反轉是你的朋友

當我第一次聽說控制反轉時,我立即意識到我找到了解決方案。 這是一個通過使用接口來獲取現有依賴項並反轉它們的概念。 接口是方法的簡單聲明。 他們沒有提供任何具體的實現。 因此,它們可以用作兩個元素之間關於如何連接它們的協議。 如果您願意,它們可以用作模塊化連接器。 只要一個元素提供接口,另一個元素為其提供實現,它們就可以在不知道彼此的情況下一起工作。 這個棒極了。

讓我們看一個簡單的例子,我們如何解耦我們的系統以創建模塊化代碼。 下圖已實現為簡單的 Java 應用程序。 您可以在此 GitHub 存儲庫中找到它們。

問題

假設我們有一個非常簡單的應用程序,它只包含一個Main類、三個服務和一個Util類。 這些元素以多種方式相互依賴。 下面,您可以看到使用“大泥球”方法的實現。 類只是相互調用。 它們是緊密耦合的,你不能簡單地取出一個元素而不觸及其他元素。 使用這種風格創建的應用程序可以讓您最初快速成長。 我相信這種風格適用於概念驗證項目,因為您可以輕鬆地玩弄事物。 然而,它不適合生產就緒的解決方案,因為即使是維護也可能是危險的,任何單一的更改都可能產生不可預知的錯誤。 下圖顯示了這個大泥球結構。

Main 使用服務 A、B 和 C,它們各自使用 Util。服務 C 也使用服務 A。

為什麼依賴注入全錯了

為了尋找更好的方法,我們可以使用一種稱為依賴注入的技術。 此方法假定所有組件都應通過接口使用。 我讀過聲稱它可以將元素解耦,但它真的如此嗎? 不,請看下圖。

以前的架構,但有依賴注入。現在 Main 使用接口服務 A、B 和 C,由它們對應的服務實現。服務 A 和 C 都使用接口 Service B 和接口 Util,由 Util 實現。服務 C 也使用接口服務 A。每個服務及其接口都被視為一個元素。

目前的情況和一團糟的唯一區別是,現在我們不是直接調用類,而是通過它們的接口調用它們。 它略微改善了彼此分離的元素。 例如,如果您想在不同的項目中重用Service A ,您可以通過取出Service A本身以及Interface A以及Interface BInterface Util來實現。 如您所見, Service A仍然依賴於其他元素。 結果,我們仍然會遇到在一個地方更改代碼和在另一個地方搞亂行為的問題。 它仍然會產生一個問題,即如果您修改Service BInterface B ,您將需要更改所有依賴它的元素。 這種方法不能解決任何問題。 在我看來,它只是在元素之上添加了一層界面。 您永遠不應該注入任何依賴項,而應該一勞永逸地擺脫它們。 為獨立萬歲!

模塊化代碼的解決方案

我相信解決依賴關係的所有主要問題的方法是完全不使用依賴關係。 您創建一個組件及其偵聽器。 監聽器是一個簡單的接口。 每當您需要從當前元素外部調用方法時,您只需向偵聽器添加一個方法並調用它即可。 該元素只允許使用文件,調用其包中的方法,以及使用主框架或其他使用的庫提供的類。 下面,您可以看到修改為使用元素架構的應用程序圖。

修改為使用元素架構的應用程序圖。主要使用 Util 和所有三個服務。 Main 還為每個服務實現了一個偵聽器,該偵聽器由該服務使用。偵聽器和服務一起被視為一個元素。

請注意,在此架構中,只有Main類具有多個依賴項。 它將所有元素連接在一起並封裝應用程序的業務邏輯。

另一方面,服務是完全獨立的元素。 現在,您可以從該應用程序中取出每個服務並在其他地方重用它們。 他們不依賴其他任何東西。 但是等等,它變得更好了:你不需要再修改這些服務,只要你不改變它們的行為。 只要這些服務做他們應該做的事情,它們就可以保持不變,直到時間結束。 它們可以由專業的軟件工程師創建,也可以是第一次使用混合了goto語句製作的最糟糕的意大利麵條代碼的程序員。沒關係,因為它們的邏輯是封裝的。 儘管它可能很可怕,但它永遠不會溢出到其他班級。 這也使您能夠在多個開發人員之間拆分項目中的工作,每個開發人員都可以獨立處理自己的組件,而無需中斷另一個開發人員,甚至不需要知道其他開發人員的存在。

最後,您可以再次開始編寫獨立代碼,就像在上一個項目開始時一樣。

元素模式

讓我們定義結構元素模式,以便我們能夠以可重複的方式創建它。

元素的最簡單版本由兩部分組成:主元素類和偵聽器。 如果要使用元素,則需要實現偵聽器並調用主類。 這是最簡單的配置圖:

應用程序中單個元素及其偵聽器的圖表。和以前一樣,App 使用元素,該元素使用其偵聽器,該偵聽器由 App 實現。

顯然,您最終需要在元素中添加更多複雜性,但您可以輕鬆地做到這一點。 只要確保你的邏輯類都不依賴於項目中的其他文件。 他們只能使用該元素中的主框架、導入的庫和其他文件。 當涉及到像圖像、視圖、聲音等資產文件時,它們也應該被封裝在元素中,以便將來它們易於重用。 您可以簡單地將整個文件夾複製到另一個項目中,就可以了!

下面,您可以看到一個顯示更高級元素的示例圖。 請注意,它由正在使用的視圖組成,並且不依賴於任何其他應用程序文件。 如果您想知道檢查依賴項的簡單方法,只需查看導入部分。 是否有來自當前元素之外的文件? 如果是這樣,那麼您需要通過將這些依賴項移動到元素中或添加對偵聽器的適當調用來刪除這些依賴項。

更複雜元素的簡單圖表。在這裡,“元素”這個詞的更大意義由六個部分組成:視圖;邏輯 A、B 和 C;元素;和元素監聽器。後兩者與App的關係和之前一樣,只是內部Element也使用了Logic A和C,Logic C使用了Logic A和Logic B,Logic A使用了Logic B和View。

讓我們看一下用 Java 創建的簡單“Hello World”示例。

 public class Main { interface ElementListener { void printOutput(String message); } static class Element { private ElementListener listener; public Element(ElementListener listener) { this.listener = listener; } public void sayHello() { String message = "Hello World of Elements!"; this.listener.printOutput(message); } } static class App { public App() { } public void start() { // Build listener ElementListener elementListener = message -> System.out.println(message); // Assemble element Element element = new Element(elementListener); element.sayHello(); } } public static void main(String[] args) { App app = new App(); app.start(); } }

最初,我們定義ElementListener來指定打印輸出的方法。 元素本身定義如下。 在元素上調用sayHello時,它只是使用ElementListener打印一條消息。 請注意,該元素完全獨立於printOutput方法的實現。 它可以打印到控制台、物理打印機或精美的 UI 中。 該元素不依賴於該實現。 由於這種抽象,這個元素可以很容易地在不同的應用程序中重用。

現在看看主App類。 它實現了監聽器並將元素與具體實現組裝在一起。 現在我們可以開始使用它了。

您也可以在此處使用 JavaScript 運行此示例

元素架構

讓我們看看在大規模應用中使用元素模式。 在一個小項目中展示它是一回事——將它應用到現實世界是另一回事。

我喜歡使用的全棧 Web 應用程序的結構如下所示:

 src ├── client │ ├── app │ └── elements │ └── server ├── app └── elements

在源代碼文件夾中,我們最初拆分了客戶端和服務器文件。 這樣做是合理的,因為它們在兩種不同的環境中運行:瀏覽器和後端服務器。

然後我們將每一層的代碼拆分到名為 app 和 elements 的文件夾中。 Elements 由具有獨立組件的文件夾組成,而 app 文件夾將所有元素連接在一起並存儲所有業務邏輯。

這樣,元素可以在不同的項目之間重用,而所有特定於應用程序的複雜性都封裝在一個文件夾中,並且通常簡化為對元素的簡單調用。

動手示例

相信實踐總是勝過理論,讓我們看一個用 Node.js 和 TypeScript 創建的真實示例。

現實生活中的例子

這是一個非常簡單的 Web 應用程序,可以用作更高級解決方案的起點。 它確實遵循元素架構,並且使用了廣泛的結構元素模式。

從高亮可以看出,主頁已經被區分為一個元素。 此頁麵包含其自己的視圖。 因此,例如,當您想重用它時,您可以簡單地複制整個文件夾並將其放入不同的項目中。 只需將所有東西連接在一起就可以了。

這是一個基本示例,說明您現在可以開始在自己的應用程序中引入元素。 您可以開始區分獨立的組件並分離它們的邏輯。 您當前正在處理的代碼有多混亂並不重要。

開發更快,更頻繁地重用!

我希望,使用這套新工具,您將能夠更輕鬆地開發更易於維護的代碼。 在開始在實踐中使用元素模式之前,讓我們快速回顧一下所有要點:

  • 由於多個組件之間的依賴關係,軟件中會出現很多問題。

  • 通過在一個地方進行更改,您可以在其他地方引入不可預測的行為。

三種常見的架構方法是:

  • 大泥球。 它非常適合快速開發,但不適合穩定的生產目的。

  • 依賴注入。 這是您應該避免的半生不熟的解決方案。

  • 元素架構。 該解決方案允許您創建獨立的組件並在其他項目中重用它們。 對於穩定的生產版本,它是可維護且出色的。

基本元素模式由一個包含所有主要方法的主類和一個偵聽器組成,該偵聽器是一個允許與外部世界通信的簡單接口。

為了實現全棧元素架構,首先將前端與後端代碼分開。 然後,您在每個文件夾中為應用程序和元素創建一個文件夾。 elements 文件夾包含所有獨立的元素,而 app 文件夾將所有內容連接在一起。

現在您可以開始創建和分享您自己的元素了。 從長遠來看,它將幫助您創建易於維護的產品。 祝你好運,讓我知道你創造了什麼!

此外,如果您發現自己過早地優化代碼,請閱讀 Toptaler Kevin Bloch的如何避免過早優化的詛咒

相關: JS 最佳實踐:使用 TypeScript 和依賴注入構建 Discord Bot