Skip to content

Commit 6023cf6

Browse files
committed
新增 Clean Architecture 分層職責的學習筆記,探討檔案上傳功能中的技術與業務邏輯劃分
1 parent 5ed2d66 commit 6023cf6

File tree

1 file changed

+215
-0
lines changed

1 file changed

+215
-0
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
---
2+
title: " [架構筆記] Clean Architecture 分層職責:一道值得深思的面試題"
3+
date: 2025/08/12 13:29:15
4+
tags:
5+
- 學習筆記
6+
---
7+
8+
## 前情提要
9+
10+
最近在 Code Review 時遇到一個有趣的討論:檔案上傳功能中,檔案編碼修復應該放在 Middleware 還是 UseCase?
11+
12+
這個問題引發了我對 Clean Architecture 分層職責的重新思考,也讓我意識到這是一個很好的面試題目。畢竟,分層架構不只是把程式碼分資料夾這麼簡單,背後有著更深層的設計哲學。
13+
14+
## 問題背景
15+
16+
在一個採用 Clean Architecture 的專案中,我們有兩個檔案上傳功能:
17+
18+
- **知識庫檔案上傳** - 支援 PDF、Word、圖片等多種格式
19+
- **合約檔案上傳** - 原始合約支援 PDF/Word,簽署後合約只支援 PDF
20+
21+
當前系統架構包含:
22+
23+
- **Middleware** - Express 中介軟體層
24+
- **Controller** - HTTP 請求處理層
25+
- **UseCase** - 業務邏輯層
26+
- **Service** - 基礎設施服務層
27+
28+
## 核心問題:分層職責如何劃分?
29+
30+
以下邏輯應該放在哪一層?
31+
32+
1. **檔案編碼修復** - 解決中文檔名亂碼問題 (latin1 → utf8)
33+
2. **檔案類型驗證** - 檢查 MIME type 是否符合業務需求
34+
3. **檔案大小限制** - 根據不同業務場景設定不同大小限制
35+
4. **JWT Token 驗證** - 檢查使用者身份
36+
5. **檔案內容解析** - 提取 PDF/Word 文件內容
37+
38+
## 分析思路
39+
40+
### Middleware 的職責:技術基礎設施
41+
42+
```typescript
43+
// ✅ 適合放 Middleware
44+
// authMiddleware.ts - HTTP 認證技術處理
45+
- JWT token 解析和驗證
46+
- HTTP 權限檢查
47+
- 將認證結果附加到 req.user
48+
49+
// errorHandler.ts - HTTP 錯誤處理
50+
- 將系統錯誤轉換為 HTTP 狀態碼
51+
- 統一錯誤格式回應
52+
- 錯誤日誌記錄
53+
54+
// uploadFiles.ts - 檔案上傳技術處理
55+
- 檔案編碼修復 (HTTP 上傳技術問題) ✅
56+
- Multer 設定和記憶體存儲
57+
```
58+
59+
### UseCase 的職責:業務邏輯驗證
60+
61+
```typescript
62+
// ✅ 適合放 UseCase
63+
// UploadKnowledgeFiles.ts - 知識庫檔案業務邏輯
64+
class UploadKnowledgeFiles {
65+
private readonly validMimeTypes = {
66+
'application/pdf': 'pdf',
67+
'text/plain': 'txt',
68+
'application/msword': 'word',
69+
'image/jpeg': 'image',
70+
'image/png': 'image',
71+
// ... 更多類型
72+
} as const
73+
74+
// 檔案類型白名單驗證 (業務規則)
75+
// 檔案大小限制檢查 (業務需求)
76+
// 檔案數量限制驗證 (業務邏輯)
77+
}
78+
79+
// UploadContractFile.ts - 合約檔案業務邏輯
80+
class UploadContractFile {
81+
// 不同類型合約的檔案限制 (業務場景)
82+
private readonly validMimeTypesForOrigin = {
83+
'application/pdf': 'pdf',
84+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'word',
85+
} as const
86+
87+
private readonly validMimeTypesForNonOrigin = {
88+
'application/pdf': 'pdf', // 簽署後只能 PDF
89+
} as const
90+
}
91+
```
92+
93+
## 設計原則
94+
95+
### Middleware 處理的是...
96+
97+
- **跨領域技術問題** (認證、編碼、錯誤處理)
98+
- **HTTP 協議相關** (請求解析、回應格式)
99+
- **基礎設施關注點** (日誌、監控、安全)
100+
- **與業務無關的技術細節**
101+
102+
### UseCase 處理的是...
103+
104+
- **特定業務場景的規則** (不同業務有不同檔案限制)
105+
- **領域知識驗證** (合約標題、內容檢查)
106+
- **業務流程邏輯** (檔案處理、資料儲存)
107+
- **動態配置的業務參數** (從環境變數讀取的業務限制)
108+
109+
## 實際案例分析
110+
111+
### 檔案編碼問題:技術問題 → Middleware ✅
112+
113+
```typescript
114+
// uploadFiles.ts
115+
const fixFilenameEncoding = (filename: string): string => {
116+
return Buffer.from(filename, 'latin1').toString('utf8')
117+
}
118+
119+
const upload = multer({
120+
storage: multer.memoryStorage(),
121+
fileFilter: (_req, file, cb) => {
122+
file.originalname = fixFilenameEncoding(file.originalname)
123+
cb(null, true)
124+
},
125+
})
126+
```
127+
128+
**為什麼放 Middleware?**
129+
130+
- 這是 HTTP 上傳過程中的技術問題,不是業務邏輯
131+
- 所有檔案上傳都需要這個處理,跨多個 UseCase
132+
- 屬於請求處理層面的責任
133+
134+
### 檔案類型限制:業務邏輯 → UseCase ✅
135+
136+
```typescript
137+
// UploadContractFile.ts
138+
class UploadContractFile {
139+
execute(files: Express.Multer.File[], isOrigin: boolean) {
140+
const validTypes = isOrigin
141+
? this.validMimeTypesForOrigin
142+
: this.validMimeTypesForNonOrigin
143+
144+
// 業務邏輯:根據合約類型決定允許的檔案格式
145+
this.validateFileTypes(files, validTypes)
146+
}
147+
}
148+
```
149+
150+
**為什麼放 UseCase?**
151+
152+
- 不同業務場景有不同的檔案類型限制
153+
- 這是領域知識,需要業務邏輯判斷
154+
- 可能隨著業務需求變化而調整
155+
156+
## 進階思考
157+
158+
### 如果檔案大小限制要動態調整呢?
159+
160+
```typescript
161+
// UseCase 層處理動態業務配置
162+
class UploadKnowledgeFiles {
163+
constructor(
164+
private configService: ConfigService,
165+
private userService: UserService
166+
) {}
167+
168+
async execute(files: Express.Multer.File[], userId: string) {
169+
const user = await this.userService.findById(userId)
170+
const maxSize = user.isVip
171+
? this.configService.get('VIP_MAX_FILE_SIZE')
172+
: this.configService.get('NORMAL_MAX_FILE_SIZE')
173+
174+
this.validateFileSize(files, maxSize)
175+
}
176+
}
177+
```
178+
179+
這樣的設計對測試也更友好:
180+
181+
```typescript
182+
// 可以輕鬆 mock 依賴,測試不同的業務場景
183+
describe('UploadKnowledgeFiles', () => {
184+
it('should allow larger files for VIP users', async () => {
185+
// Arrange
186+
mockUserService.findById.mockResolvedValue({ isVip: true })
187+
mockConfigService.get.mockReturnValue(100 * 1024 * 1024) // 100MB
188+
189+
// Act & Assert
190+
await expect(uploadUseCase.execute(largeFiles, 'vip-user-id'))
191+
.resolves.not.toThrow()
192+
})
193+
})
194+
```
195+
196+
## 總結
197+
198+
好的架構設計不是憑感覺,而是要有明確的原則:
199+
200+
- **Middleware** = 技術基礎設施,處理 HTTP 層面的問題
201+
- **UseCase** = 業務邏輯驗證,處理領域相關的規則
202+
203+
這種分層不只讓程式碼更好維護,也讓測試更容易撰寫,更符合單一職責原則。
204+
205+
下次在設計架構時,不妨問問自己:
206+
207+
- 這個邏輯是技術問題還是業務問題?
208+
- 這個規則會因為業務需求變化嗎?
209+
- 這個處理邏輯需要在多個地方重複嗎?
210+
211+
Clean Architecture 的精神就在於:**讓業務邏輯獨立於技術細節**
212+
213+
面試時遇到類似問題,記得從職責分離的角度思考,相信你會有不錯的表現!
214+
215+
(fin)

0 commit comments

Comments
 (0)