---
title: "架構規劃 - Clean Architecture in TypeScript"
date: 2025-08-15T00:00:00+08:00
publishDate: 2025-08-15T00:00:00+08:00
lastmod: 2025-11-21T10:38:12+08:00
tags: ["TypeScript","Clean Architecture","架構","經驗","AI"]
series: "clean-architecture-in-ts"
toc: true
permalink: "https://blog.aotoki.me/posts/2025/08/15/clean-architecture-in-ts-architecture-planning/"
language: "zh-tw"
---



今年（2025 年） [ThroughWorks](https://www.thoughtworks.com/) 的 Technology Radar 中提及了 [AI-friendly code design](https://www.thoughtworks.com/content/dam/thoughtworks/documents/radar/2025/04/tr_technology_radar_vol_32_en.pdf) 的概念，討論以往軟體開發的優良實踐，也能夠對 AI 協力開發產生幫助，因此列入的評估的範圍中，在為來可能會成為採用的選項之一。

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

<!--more-->

## Clean Architecture

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

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

然而，抽象化會導致「[認知負擔（Cognitive load）](https://minds.md/zakirullin/cognitive)」但是適當的抽象化處理，可以很好地提高軟體的可維護性，因此抽象化的核心就在於我們該怎麼界定抽象化處理。

## 商業邏輯{#business-logic}

在 [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 中使用了四個環的範例來切分，然而本質上是將商業邏輯跟非商業邏輯的部分一分爲二，透過定義介面（Interface）進行抽象化，接著實作 Interface Adapter 來讓非商業邏輯的部分能被商業邏輯使用。

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

```ts
class CartController {
  // ...
  async post(params): Promise<Response> {
    // ...
    const res = await this.db.execute(
	    "INSERT INTO cart_items (cart_id, product_id, name, price) VALUES (?, ?, ?, ?)",
	  cartItem.cartId,
	  cartItem.productId,
	  cartItem.name,
	  cartItem.price
    )
    // ...
  }
}
```

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

> 雖然 AI 可以很好地理解這些，但同時也在擴大 Context（脈絡）的需求，那麼就有可能發生[注意力遺失（Loss Attention）](https://www.chatpaper.ai/zh-Hant/dashboard/paper/cecc1c4b-38a0-44cf-8557-b580600a6845)這類現象，這也會影響到我們在程式碼生成的成效。

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

```ts
interface CartItemRepository {
  save(item: CartItem): Promise<void>
}

class CartController {
  // ...
  async post(params): Promise<Response> {
    // ...
	await this.repository.save(cartItem)
    // ...
  }
}
```

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

## 架構規劃{#architecture-plan}

基於上述的切分方式，我們可以再更近一步對非商業邏輯的部分進行細分，這些分類大多是遵照[單一職責原則](https://zh.wikipedia.org/zh-tw/%E5%8D%95%E4%B8%80%E5%8A%9F%E8%83%BD%E5%8E%9F%E5%88%99)來劃分的，至少我們不會希望處理 API 請求的行為跟資料庫的處理混在一起。

我們可以沿用 [Clean Architecture in Go](https://blog.aotoki.me/series/clean-architecture-in-go/) 的規劃經驗，這一次的專案大致上我們會有以下幾種物件類型。

商業邏輯類

```
src/
├─ usecase/
├─ entity/
```

非商業邏輯

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

前端（非商業邏輯）

```
src/
├─ api/
├─ view/
```

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

```md
根據規格文件 `docs/get_cart_items.md` 中的描述，實作一個 API 端點 `/api/cart_items` 你需要修改以下檔案

* 更新 `CartItemRepository` 的 Postgres 實作，加入讀取資料的行為
* 加入 `ListCartItem` 的 UseCase
* 加入 `JsonCartItemPresenter` 的實作
* 更新 `CartItemController` 支援讀取 CartItem 並且回傳 JSON
* 更新 `src/config/routes.ts` 加入新的端點

團隊的慣例，請參考 `docs/convention.md`
```

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

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