蒼時弦也
蒼時弦也
資深軟體工程師
發表於

架構規劃 - Clean Architecture in TypeScript

這篇文章是 Clean Architecture in TypeScript 系列的一部分,你可以透過 Leanpub 提前閱讀內容。

今年(2025 年) ThroughWorks 的 Technology Radar 中提及了 AI-friendly code design 的概念,討論以往軟體開發的優良實踐,也能夠對 AI 協力開發產生幫助,因此列入的評估的範圍中,在為來可能會成為採用的選項之一。

即使我們仍不確定未來會如何變化,但是在良好的軟體架構跟實踐為基礎,仍可以幫助我們更好地應用 AI 來進行開發。

Clean Architecture

對於使用大型語言模型(LLM,Large Language Model)提供脈絡(Context)可以改善表現已經是常識一樣的概念,那麼在軟體中採用容易理解的架構也能有不錯的效果,而 Clean Architecture 就是其中一種可行的方式。

過去幾年思考和嘗試 Clean Architecture 的應用後,得出了一個很容易在初期上手的技巧——將商業邏輯抽象化。

然而,抽象化會導致「認知負擔(Cognitive load)」但是適當的抽象化處理,可以很好地提高軟體的可維護性,因此抽象化的核心就在於我們該怎麼界定抽象化處理。

商業邏輯

Clean Architecture 中使用了四個環的範例來切分,然而本質上是將商業邏輯跟非商業邏輯的部分一分爲二,透過定義介面(Interface)進行抽象化,接著實作 Interface Adapter 來讓非商業邏輯的部分能被商業邏輯使用。

舉例來說,我們想要將一個叫做 CartItem 的物件保存起來,如果沒有使用上述的技巧,實作可能是這樣的。

 1class CartController {
 2  // ...
 3  async post(params): Promise<Response> {
 4    // ...
 5    const res = await this.db.execute(
 6	    "INSERT INTO cart_items (cart_id, product_id, name, price) VALUES (?, ?, ?, ?)",
 7	  cartItem.cartId,
 8	  cartItem.productId,
 9	  cartItem.name,
10	  cartItem.price
11    )
12    // ...
13  }
14}

我們會直接取用跟商業邏輯無關的元件(資料庫)來處理,這樣會造成一些問題,像是跟使用 SQL 的資料庫機制耦合、暴露過多細節等等。

雖然 AI 可以很好地理解這些,但同時也在擴大 Context(脈絡)的需求,那麼就有可能發生注意力遺失(Loss Attention)這類現象,這也會影響到我們在程式碼生成的成效。

若要改善這個問題,我們只需要將商業邏輯(購物車品項管理)跟非商業邏輯(資料保存)加入抽象化處理,在 TypeScript 中只需要定義一個介面就可以達到必要的效果。

 1interface CartItemRepository {
 2  save(item: CartItem): Promise<void>
 3}
 4
 5class CartController {
 6  // ...
 7  async post(params): Promise<Response> {
 8    // ...
 9	await this.repository.save(cartItem)
10    // ...
11  }
12}

那麼,不論是使用 RDBMS(關聯式資料庫)還是 NoSQL 的方式保存資料,都跟當下的商業邏輯無關,我們只關注「購物車品項需要持久保存」這一件事情,因為這是我們的商業需求。

架構規劃

基於上述的切分方式,我們可以再更近一步對非商業邏輯的部分進行細分,這些分類大多是遵照單一職責原則來劃分的,至少我們不會希望處理 API 請求的行為跟資料庫的處理混在一起。

我們可以沿用 Clean Architecture in Go 的規劃經驗,這一次的專案大致上我們會有以下幾種物件類型。

商業邏輯類

src/
├─ usecase/
├─ entity/

非商業邏輯

src/
├─ controller/
├─ repository/
├─ presenter/
├─ service/
├─ agent/

前端(非商業邏輯)

src/
├─ api/
├─ view/

後續的章節會進一步說明每一個元件的應用,基本上我們只需要了解到當我們擁有這樣的架構規劃後,就能讓 AI 代理(Agent)透過類似這樣的提示(Prompt)來實作。

1根據規格文件 `docs/get_cart_items.md` 中的描述,實作一個 API 端點 `/api/cart_items` 你需要修改以下檔案
2
3* 更新 `CartItemRepository` 的 Postgres 實作,加入讀取資料的行為
4* 加入 `ListCartItem` 的 UseCase
5* 加入 `JsonCartItemPresenter` 的實作
6* 更新 `CartItemController` 支援讀取 CartItem 並且回傳 JSON
7* 更新 `src/config/routes.ts` 加入新的端點
8
9團隊的慣例,請參考 `docs/convention.md`

上述有不少描述可以利用 IDE(如:VSCode、Cursor)的提示樣板功能標準化,但大多數情境就足夠讓 AI 完成一定程度的實作,透過限定每個檔案的功能,可以有效的降低錯誤機率和遵守實作的標準。

這些都會是讓 AI 協作的品質可控,也能夠在較大規模修改下還能保持可審查(Code Review)的基礎。