Nx架構案例分享-為什麼app要跟商業邏輯分離?

Nx架構案例分享-為什麼app要跟商業邏輯分離?

·

2 min read

最近團隊導入了Monorepo的框架-Nx。在Nx中有多種project型態,大致上可分為兩種:application和library。官方文件有提到兩種project的使用方法

A common mental model is to see the application as "containers" that link, bundle and compile functionality implemented in libraries for being deployed. As such, if we follow a 80/20 approach:

  • place 80% of your logic into the libs/ folder
  • and 20% into apps/

也就是大部分的商業邏輯應該放在library,而application應視為商業邏輯的容器。 文件有提到為什麼會這樣區分,但沒有分享實際案例做輔助。前陣子剛好遇到高度相關的需求,充滿了許多血淋淋的案例,就趁這個機會記錄成文章,希望可以讓使用Nx的同好作為參考。

不確定初期

團隊需要開發一個全新的產品,產品類型是一個像大廳的網站,先簡稱lobby好了。 由於產品的商業模式蠻新的,處於邊走邊調整的時期,這個lobby站有可能會跟據客戶需求發展成多個lobby站,每個lobby站會有各自客製化的部分,但會有大部分共用的功能。團隊評估商業模式可能的發展路線後決定導入Nx,希望能藉由Nx拆分app跟lib層的特性保留開發彈性。將來確定有多個app的需求也可以輕鬆復用商業邏輯。

當時認為多個app的需求還未確定,只要先把商業邏輯分好放在app中的lib folder,將來有需要再整包lib拉出來變成一包domain library就好。這時只有一些不包含domain的工具被抽成util library 於是初期的架構如下圖

nx initial.excalidraw.png

一個domain包含什麼?

  • Component: 使用domain邏輯的UI元件。幾乎沒有邏輯,取而代之的是跟custom hook做邏輯互動。如果只是單純顯示資料也會直接跟store拿取或訂閱
  • Hook: 包裝domain邏輯的react custom hook。主要實作流程與大部分邏輯,並提供介面給UI使用
  • Repository: 拿外部資料(ex: firestore, api)的封裝class
  • Store: 邏輯相關的狀態,使用zustand實作
  • Util: 不需要react runtime的純邏輯運算單位,基本上是pure function不會保存狀態 以上這些角色也會定義他們各自的Model與interface 架構大致上如下圖

domain.excalidraw.png

需求開始變更

後來商業需求發生預期中的變動,需要拆分多個lobby site。 確定需求如下

  • 需要有一個新的lobby site,目前會復用大部分邏輯 & UI,也有各自客製化的邏輯
  • 將來兩個lobby site可能會有更多各自獨立的需求
  • 需要與UI Developer合作(簡稱ID),ID會負責實作UI與animation,雙方會平行開發

需求確定後我們開始搬移domain,但過程中遇到一些挑戰沒辦法單純整包lib做copy paste,像是import path需要大量更改 (img path, module path等等),不好明確定義lib export出去的module等等,這時我們發現抽出domain lib需要一些明確的計畫與步驟。

抽出domain library

原本在app內的lib folder有非常多的domain。由於原本的lobby是已在營運的狀態,我們最後選擇將各個獨立的domain分成多個domain lib,而不是將所有domain放在一包很大的domain lib,這樣在拆分domain的時候我們可以小步驗證是否拆壞,也可以保留各個domain可以讓其他app使用的彈性。 拆分順序就先從最小最簡單沒有複雜dependency的domain開始,步驟大概如下

  1. 新增domain lib
  2. 將獨立domain從lib folder內拆出
  3. 更改domain lib裡的import path
  4. 根據import error開放domain lib export出去的介面
  5. 更改其他project的import path
  6. build專案驗證dependency沒問題

所有domain lib抽完之後,我們跑nx dep-graph檢查dependency分佈,也跟據lint rule nx-enforce-module-boundariesnx run-many --all --target=lint 檢查是否有dependency迴圈或其他問題

抽出lib後才發現原來lib可以設定buildable,可以將pre-compiled output當作reference,避免每次build project都再做一次compile,但在Nx官方文章內提到Incremental Builds並不適合所有application size。目前無法判定專案大小的標準就沒有全面採用,之後會再做研究跟實驗

import path需要大量更改

原本import的方式是用non-relative import。在根目錄的tsconfig.base.json裡設定app的path。

{  
  //other configs
  ...
  "compilerOptions": { 
    //other configs
    ...
    "paths": {  
      "@lobby/*": ["apps/lobby/*"], 
    }  
  },  
  "exclude": ["node_modules", "tmp"]  
}
// 需要改寫,因為domain已經不在lobby app裡
import { domain } from "@lobby/lib/domain"

但搬出lib後發現每個import path幾乎都要重新改寫,在一番折騰後我們將每個lib定義一個獨自的path,跨project的dependency我們使用ts.config定義的path import,但import同一個project的模組我們使用relative path。背後想法是如果之後還需把lib拆小或搬移內容,新lib的import path修改是必然的,但拆出去的lib通常內部module的相對位置是不會變的,至少project內的module就不用再修改各自的relative path

{  
  //other configs
  ...
  "compilerOptions": { 
    //other configs
    ...
    "paths": {  
      "@lobby/*": ["apps/lobby/*"], 
      "@lobby/domain-a": ["libs/lobby/domain-a/src/index.ts"],
      "@lobby/domain-b": ["libs/lobby/domain-b/src/index.ts"],
    }  
  },  
  "exclude": ["node_modules", "tmp"]  
}
// lobby app import lobby/domain-a
import { domainA } from "@lobby/domain-a"

//domain-a module import another domain module
import { store } from "../store/index.ts"

從import path需要大量修改的結果看來,可能同專案module使用relative path import彈性會比較大。但不確定是否Nx可以設定lib與app各自ts的baseurl與path,待研究。

抽出UI lib

由於UI跟domain共存在同一個lib dependency會很難處理,且UI跟邏輯是跨團隊分開實作。 我們後來拆出了一個獨立的UI lib,內容如下

  • component: UI元件,根據domain以及顯示區域分folder
  • style: 共用元件樣式,RWD設定等等
  • assets: 圖片或是animation素材
  • UI toggle store: 純控制UI出現條件的store

以下是目前的project種類與各自的內容

UI lib.excalidraw.png


最後總算將架構整理成有彈性且可以分工的狀態,app可以跑起來運行的那一刻非常感動。

nx final.excalidraw.png

wrap up

Nx是根據需求導入的新技術,隨著需求變更我們遇到了許多挑戰,但也累積更多經驗。

反思後也學到既然已使用Nx保留擴充彈性,就算不確定是否會拆多個app,也該照原本的設計理念將domain跟runtime拆分。一開始沒有保留彈性就等於沒有彈性,將來也會需要花費蠻多心力才能調整成有彈性的架構,我相信不論是用Nx或是其他技術都應該在一開始打好底子不偷懶,才能真正拿到架構設計上的好處。

Nx包含的層面很廣,需要持續根據商業需求以及組織分工調整架構才能發揮架構本身的價值,本質上也是讓大家工作更加順利。 這次遇到的大部分是技術使用上的問題,而不是技術本身的問題。由於需考慮專案時程以及跨團隊合作的時間配合,上述的問題處理只有處理到可以開發的程度但也沒到非常到位,之後再持續做研究跟優化,也會再做分享。

reference