Skip to content

Commit 3e1cf5b

Browse files
committed
新增 Clean Architecture 思考文章,探討避免過度設計的原則與實踐
1 parent dce38c7 commit 3e1cf5b

File tree

1 file changed

+175
-0
lines changed

1 file changed

+175
-0
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
---
2+
title: " [實作筆記] Clean Architecture 思考:避免過度設計"
3+
date: 2025/09/11 15:23:42
4+
tags:
5+
- 實作筆記
6+
---
7+
8+
## 前情提要
9+
10+
在實作強型別語言,經常會遇到一些僅次於命名的問題:
11+
12+
到底要建立多少層級的 DTO/型別?
13+
14+
什麼時候該抽象,什麼時候該保持簡單?
15+
16+
這篇文章記錄一個前端要求帶來的反思,如何在「架構純粹性」與「實用主義」之間找到平衡點?
17+
18+
## 問題場景
19+
20+
系統架構為 TypeScript 並實作 Clean Architecture。
21+
22+
我們有一個比對系統,Domain Entity 中的 `statusCode` 定義為 `string | null`
23+
24+
但前端要求 API 必須回傳 `string` 型別,允許空字串而不處理 null。
25+
26+
```typescript
27+
// Domain Entity
28+
export class Comparison {
29+
statusCode: string | null // 業務邏輯:可能為空
30+
}
31+
32+
// 前端期望的 API 回應
33+
{
34+
"statusCode": "" // 必須是 string,不能是 null
35+
}
36+
```
37+
38+
## 解決方案演進
39+
40+
### 第一版:UseCase 層建立完整 DTO
41+
42+
```typescript
43+
// 建立回應 DTO
44+
export interface ComparisonResponseDTO {
45+
id: string
46+
statusCode: string // 轉換為 string
47+
// ... 其他欄位
48+
}
49+
50+
// 建立 UseCase 回應型別
51+
export type GetComparisonListResDTO = IDataAndCount<ComparisonResponseDTO>
52+
53+
// UseCase 中處理轉換
54+
private toResponseDTO(comparison: Comparison): ComparisonResponseDTO {
55+
return {
56+
// ...
57+
statusCode: comparison.statusCode || '',
58+
// ...
59+
}
60+
}
61+
```
62+
63+
看起來很「Clean Architecture」,但真的有必要嗎?
64+
65+
關鍵觀點,有沒有可能過度設計了?
66+
67+
### 最終方案:Controller 邊界處理
68+
69+
```typescript
70+
// Controller 直接處理轉換
71+
async getComparisonList(req: Request, res: Response): Promise<void> {
72+
const { data, count } = await this.getComparisonListUseCase.execute(input)
73+
74+
res.status(200).json({
75+
data: data.map(comparison => ({
76+
...comparison,
77+
statusCode: comparison.statusCode || ''
78+
})),
79+
pagination
80+
})
81+
}
82+
```
83+
84+
## 避免型別地獄的原則
85+
86+
### 1. YAGNI 原則 (You Aren't Gonna Need It)
87+
88+
不要為了「完整性」而建立無意義的型別包裝
89+
90+
或是建立過多的 Mapper 類別
91+
92+
```typescript
93+
// ❌ 過度抽象
94+
export type GetComparisonListResDTO = IDataAndCount<ComparisonResponseDTO>
95+
96+
// ✅ 直接使用泛型
97+
IUseCase<GetComparisonListReqDTO, IDataAndCount<ComparisonResponseDTO>>
98+
```
99+
100+
### 2. 什麼時候需要抽象化?
101+
102+
我的判斷,重複的時候,例如,當 2 個以上的 API 需要相同的型別時,才考慮抽象
103+
104+
也不排除可以能轉換極度複雜,這時有一個 Mapper 的導入反而可以減輕負擔時才導入。
105+
106+
架構/Design Pattern 應該服務 RD 而不是折摩 RD
107+
108+
```typescript
109+
// 如果只有一個 API 使用,直接 inline
110+
data.map(item => ({ ...item, statusCode: item.statusCode || '' }))
111+
112+
// 如果多個 API 都需要,才建立共用函數或型別
113+
const transformComparison = (item: Comparison) => ({
114+
...item,
115+
statusCode: item.statusCode || ''
116+
})
117+
```
118+
119+
### 3. 複雜度評估
120+
121+
簡單的轉換邏輯不需要額外抽象:
122+
123+
```typescript
124+
// ✅ 簡單轉換,直接處理
125+
statusCode: comparison.statusCode || ''
126+
127+
// ❌ 為簡單邏輯建立複雜抽象
128+
private transformStatusCode(statusCode: string | null): string {
129+
return statusCode || ''
130+
}
131+
```
132+
133+
## 實用主義 vs 理論完美
134+
135+
### 現代 TypeScript 最佳實踐
136+
137+
1. **直接使用泛型** - 避免不必要的 type alias
138+
2. **減少型別層級** - 除非有明確的業務意義
139+
3. **保持簡潔** - 不為了「完整性」而建立無意義的包裝
140+
141+
### 架構決策的平衡點
142+
143+
| 考量因素 | 過度設計 | 適度設計 | 設計不足 |
144+
|---------|---------|---------|---------|
145+
| 型別數量 | 為每個 UseCase 建專屬 DTO | 共用 + 泛型 | 沒有型別安全 |
146+
| 轉換位置 | 每層都轉換 | 邊界處理 | 隨意放置 |
147+
| 複雜度 | 型別地獄 | 恰到好處 | 難以維護 |
148+
149+
## 進階判斷:何時需要抽象,何時保持簡單?
150+
151+
| 判斷角度 | 適合抽象化的情境 | 適合保持簡單的情境 |
152+
|----------------|------------------|--------------------|
153+
| **使用頻率** | 多個 UseCase 或 API 重複出現 → 建立共用型別/函數 | 僅在單一 Controller 出現一次 → inline 即可 |
154+
| **業務語意** | 欄位轉換具有業務意義(例:狀態機的一環) → 抽象進 Domain/UseCase | 純展示需求(例:`null → ""`) → 邊界層處理 |
155+
| **團隊規模與可讀性** | 大團隊、專案壽命長 → 適度抽象,降低未來重構風險 | 小團隊、短期專案 → 保持簡單,降低溝通成本 |
156+
| **錯誤影響範圍** | 錯誤會影響商業邏輯(例:金額精度) → 上升到 UseCase/Domain | 只影響 API 輸出表現(例:空字串 vs. null) → Controller 處理 |
157+
| **演進空間** | 需求可能持續變化(欄位規則增多/不同前端需求) → 抽象留彈性 | 需求相對穩定 → inline 保持簡潔 |
158+
159+
160+
## 小結
161+
162+
Clean Architecture 的精神是**將商業邏輯集中在核心,邊界負責格式適配**
163+
164+
不要被「層級完整性」綁架,重點是:
165+
166+
1. **Domain 保持純粹** - 反映真實的業務狀態
167+
2. **UseCase 專注邏輯** - 編排業務流程,不處理格式
168+
3. **Controller 處理邊界** - HTTP 格式轉換在此進行
169+
4. **實用主義優先** - 簡單的需求用簡單的方法
170+
171+
記住:**改動越少越好,沒有必要就不要建一大堆型別**
172+
173+
架構是為了解決問題,不是為了展示理論知識。當實用主義與理論衝突時,選擇能讓團隊更有生產力的方案。
174+
175+
(fin)

0 commit comments

Comments
 (0)