多應用程式專案,通常稱為傘式專案,是大多數專案在商業環境中設定的結構,主要是因為它們能讓維護單一儲存庫中的多個 OTP 應用程式變得更容易。在本章中,我們將涵蓋結構、最有用和最沒用之處,一些此新結構的細微差別,最後提供將巨型程式庫適當地拆分成多個 OTP 應用程式的提示。
組織多應用程式專案
傘式專案的一個範例是我們之前用過的 service_discovery 儲存庫。讓您立即知道它是傘式的明確標誌在目錄清單中可見
$ ls
apps cloudbuild.yaml deployment Dockerfile README.md rebar.lock Tiltfile
ci config docker-compose.yml LICENSE rebar.config test
多應用程式專案需要一個目錄,所有頂層應用程式的原始碼都位在其中,這個目錄在 apps/
目錄 (也支援 libs/
目錄)。每當您看到 apps/
或 libs/
中有 rebar.config
檔案,就能相當確定這是傘式專案。
看看那個目錄,您就能相當明確地了解專案的目的是什麼
$ ls apps
service_discovery service_discovery_http service_discovery_storage
service_discovery_grpc service_discovery_postgres
關於如何命名專案中的 OTP 應用程式,並沒有硬性規定。我們發現用於體驗的規則總是使用某種命名空間,因為 VM 不支援任何此類內容。在上一個清單中,我們可以看到我們有一個 service_discovery
應用程式,然後有一堆 service_discovery_<thing>
應用程式。
這告訴我們這些應用程式都是相關的。主要應用程式可能是 service_discovery
,而其他應用程式是輔助程式:_grpc
和 _http
應用程式可能是前端或用戶端程式庫 (劇透:它們是前端);_storage
應用程式應當清楚地處理儲存;_postgres
應用程式也可能處理某種儲存。
如果您去深挖程式碼,您會發現 _storage
是一種通用儲存 API (見其 .app.src
檔案的 description
欄位),而 _postgres
是具體實作之一:它已針對可擴充性進行撰寫。
應用程式有可能在目錄中使用非常不同的名稱。例如,我們可以決定撰寫或供應某種身分驗證函式庫,所以我們也可以在其中包含類似 authlib
和 authlib_http
的應用程式。
這就是為何在大型專案中命名空間很有用的原因。當專案擴充時,它們往往會獲得更多和更多 API、端點、客戶端,以及與它們互動的方法,而在撰寫時雖然很繁瑣,但此類手動命名空間必要時會提供非常明確的分隔。
在所有案例中,這個多應用程式目錄是單一應用程式和多應用程式專案之間最大的結構差異。不過,還有一個細微的差異:你可以有多個 rebar.config 和測試目錄。特別是在 service_discovery
的案例中,你可以看到它有一個頂層 rebar.config
檔案和一個 test/
目錄。但如果你深入查看所有的個別應用程式,就會有更多
$ ls apps/service_discovery*
apps/service_discovery:
src
apps/service_discovery_grpc:
proto rebar.config src
apps/service_discovery_http:
src
apps/service_discovery_postgres:
priv src
apps/service_discovery_storage:
src
所有應用程式都維持 OTP 應用程式基本需要的 src/
目錄,但它們可以任意新增自己的測試目錄、priv/
目錄,或任何它們需要的目錄,以及新的 rebar.config
檔案。
我們來看看 service_discovery_grpc
的設定檔
{grpc, [{protos, "proto"},
{gpb_opts, [{descriptor, true},
{module_name_prefix, "sdg_"},
{module_name_suffix, "_pb"}]}]}.
此組態專門針對頂層 rebar.config
中宣告的 grpcbox_plugin
,但允許該外掛程式僅針對會需要它的 OTP 應用程式執行。
簡而言之,這會為建置和架構應用程式創造多層動態
- 頂層的每樣東西都會共用在所有頂層應用程式中(甚至測試)
- 每個應用程式都可以透過建立目錄或測試檔案的當地版本來允許更加具體
這裡有幾個例外。例如,專案的依賴項共用:雖然每個頂層應用程式都可以指定自己的依賴項,但 Erlang 和 Rebar3 一次只允許載入一個版本的函式庫(暫時排除即時程式碼升級)。這表示依賴項解析將為所有衝突的版本挑選一個獲勝的應用程式,因此將它們視為共用是有意義的。相反地,像是 priv/
之類的目錄是針對單一應用程式的私人檔案而設計,雖然任何人都可以讀取其內容,但沒有多個應用程式可以擁有同一個目錄。
另一個細微差別是在 hooks;有些 hooks 可以同時針對單一 OTP 應用程式和整個專案定義。例如,在頂層定義的 compile
屬性會在建置所有應用程式之前或之後執行,而為 apps/
中單一應用程式定義的同一個屬性只會在建置該一個應用程式之前或之後執行。
是否應該遷移
與數十個單一應用程式儲存庫相較,雨傘專案的主要好處在於它讓您大部分的程式碼開發集中於單一處,而在此處您可以輕易地為許多工具套用一個共用的配置設定。這還會讓您從檢視、移植和歷史方面在單一儲存庫中追蹤所有專案變得很容易。這聽起來像是一個蠻直接的決定,但它總是不那麼容易。
切換至多應用程式專案有兩個重要的注意事項。第一個是 Erlang 專案使用 Rebar3 時可以擁有的唯一相依性,至少在新功能加入以允許它之前,必需是單一應用程式儲存庫。如果您打算編寫要在您的工作場所中多個專案之間使用的函式庫,在多應用程式專案中進行此操作將無法運作,除非所有開發作業也移至多應用程式專案中。
第二個注意事項是將您所有的開發移至大型多應用程式專案中也不容易。大多數工具假設您可能會為每個專案建置一個發行版本(或不超過幾個版本),因此將不猶豫執行程式碼分析或同時重新建置所有上層應用程式。
這表示如果您的規模不大的儲存庫不是幾個,而是巨大的一個,您可能會看到一般的指令需要更多時間,只因為他們預期巨大的專案比較少見。
在 Rebar3(以及其他工具)能夠趕上大型程式庫之前,您可能會想要按照下列方式進行結構化
- 每個大型專案(例如服務或微服務)製作一個多應用程式儲存庫
- 所有您在大型專案間共用的函式庫會個別維護並發布,並在特定專案需要時拉入
- 在一些您的團隊成員會為自動處理服務、網路 API、CI 配置等之配置樣式和規格全球安裝的某個一般外掛程式中儲存範本
如果在某個時間點來自多應用程式專案的函式庫開始對您的組織中的其他使用者有用,便可以輕易地將其新增至自己的儲存庫中、發布它、並重新匯入它作為相依性。同樣地,遺棄的函式庫或分支的函式庫可以在每個專案中於當地進行維護。
當您的組織計畫進行修補或開發,然後再發布開源程式碼時,此結構也有幫助,並讓您只需進行一次變更,就能對函式庫進行變更,而無需一次與所有使用者同步。
在針對每個專案開發部署和 CI 的特定腳本時,此舉也使事情變得相對簡單;開放原始碼工具往往保持良好運作。不過,對於已針對這些事項開發單一儲存庫和工具的組織來說,此舉的成本較高。另一個常見障礙是,它要求一個專案的 CI 和建置伺服器具有對其相依項項目的讀取存取權,並非所有組織都一定已具備此功能。
將應用程式切分
不論您偏好在哪一種方法(單一應用程式、多個應用程式或單一儲存庫),您都必須找出最適合將程式碼切成可管理區塊的方式。
無關您撰寫內容為何,這向來都是項挑戰。就如同有數不清的部落格文章探討函數的完美大小、模組或類別應包含和公開哪些內容,以及微型服務應有多大或多小,對於 OTP 應用程式的最佳大小,並無任何單一的規範性參考文獻。
身為結構設計者,我們傾向依據某種關於良好隔離感覺的直覺,而不是提供死板快速的規則。這通常是難以教授的經驗,以下是我們喜歡提問的幾個問題,可簡化決策程序
- 特定的功能單元是否為其他專案最終可能需要之功能?如果是,提供此功能自己的 OTP 應用程式可能會有所助益,因為這樣可輕鬆擷取和分享
- 特定功能單元是否包含專案關注事項的非常具體程式碼?針對服務發現,它是否與追蹤服務有關,或類似「資料儲存」的一般事項?特定性愈高,它愈應該接近主應用程式;一般性愈高,就愈容易想像成獨立的 OTP 應用程式
- 特定功能單元是否需要一些特殊的組態值或領域知識?如果是,將所有呼叫組成自己 OTP 應用程式中的受限模組集會是個好主意,讓其他人可將其用作良好的抽象來源(例如,處理驗證或特定通訊協定)
- 您可否想像它僅在某些特定情境或建置中啟動?如果是,讓它變成一個不同的應用程式會讓事情在後續執行時更加容易。此類項目的良好範例可能是健康檢查或監控端點,它們可能會依賴您的主應用程式,但不會針對其測試或特定建置而需要
所有這些問題都是代理,應引導您更輕鬆地評估您的業務邏輯(往往持續存在於頂層儲存庫和應用程式中)與您目前撰寫的其他程式碼之間存在多少關聯性。
在砂地上描繪這條線常常是一種找出如何進行結構設計的有用練習。
以 service_discovery
為例,在技術上並不要求將 service_discovery_storage
設定為與主要 service_discovery
應用程式不同的應用程式。不過,我們認為讓主要應用程式不必處理儲存層的具體事項比較有道理,不論該儲存層是一般的、 respaldado a disco、在其他服務上或在記憶體中,應用程式都不會在意。所有那些複雜性和間接性都可以透過明確定義的應用程式(service_discovery_storage
)來處理,而這個應用程式可以使用特定的外掛(例如 service_discovery_postgres
函式庫)設定。
我們覺得這個隔離有助於更清楚地確保主要應用程式永遠不必處理儲存特定的問題,而只需要以一般的抽象方式呼叫它們即可。可交換後端的複雜性仍然存在,但已被隔離在程式碼的一塊明確標籤區域中,我們希望這樣可以簡化維護。
最終,結構化與組織程式碼和存放庫的最佳方式是選擇個人和團隊在目前的組織架構下最有效的方式。我們(作者)偏好的方式是我們發現可以合理折衷各種我們過去十多年來任職組織的方式之一,但這可能不如完全採用目前提供給你的方式有效。
一次解決一個問題;如果你的團隊是第一次學習 Erlang,了解組織現有的部署和建置系統之後,從最容易的著手可能是比較有道理的,充分了解你之後會重新調整它們以求更舒適。當你的精力、時間或資源有限時,一次處理所有問題並嘗試在第一次就完美地解決所有問題會適得其反。