Skip to content

Commit 9da6756

Browse files
committed
更新 Clean Architecture 分層職責的學習筆記,修正檔案上傳功能的討論內容,強調技術與業務邏輯的分離
1 parent 21f8780 commit 9da6756

File tree

1 file changed

+34
-134
lines changed

1 file changed

+34
-134
lines changed

source/_posts/2025/backend-architecture-layer-design-principles.md

Lines changed: 34 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ tags:
77

88
## 前情提要
99

10-
最近在 Code Review 時遇到一個有趣的討論:檔案上傳功能中,檔案編碼修復應該放在 Middleware 還是 UseCase?
10+
最近在 Code Review 時遇到一個有趣的題目:
1111

12-
這個問題引發了我對 Clean Architecture 分層職責的重新思考,也讓我意識到這是一個很好的面試題目。畢竟,分層架構不只是把程式碼分資料夾這麼簡單,背後有著更深層的設計哲學。
12+
檔案上傳功能中,檔案編碼修復應該放在 Middleware 還是 UseCase?
13+
14+
這個問題引發了我對 Clean Architecture 分層職責的重新思考,與團隊背後的設計哲學。
1315

1416
## 問題背景
1517

@@ -35,71 +37,32 @@ tags:
3537
4. **JWT Token 驗證** - 檢查使用者身份
3638
5. **檔案內容解析** - 提取 PDF/Word 文件內容
3739

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 處理的是...
40+
分析思路 --- 職責角度
41+
42+
### Middleware 的職責:偏向基礎設施
43+
44+
控制進出流程 → 例:API 請求進來先檢查 Token
45+
46+
過濾與轉換資料 → 例:將日期字串轉成標準格式
47+
48+
保護系統邊界 → 例:攔截未授權的存取
49+
50+
以下不舉例:
9651

9752
- **跨領域技術問題** (認證、編碼、錯誤處理)
9853
- **HTTP 協議相關** (請求解析、回應格式)
9954
- **基礎設施關注點** (日誌、監控、安全)
10055
- **與業務無關的技術細節**
10156

102-
### UseCase 處理的是...
57+
### Use Case 的職責:偏向商業邏輯
58+
59+
驅動核心行為 → 例:建立一筆訂單流程
60+
61+
執行業務規則 → 例:檢查庫存是否足夠
62+
63+
協調內外資源 → 例:呼叫付款服務並更新資料庫
64+
65+
以下不舉例:
10366

10467
- **特定業務場景的規則** (不同業務有不同檔案限制)
10568
- **領域知識驗證** (合約標題、內容檢查)
@@ -108,90 +71,29 @@ class UploadContractFile {
10871

10972
## 實際案例分析
11073

111-
### 檔案編碼問題:技術問題 → Middleware ✅
74+
### 檔案編碼問題
11275

113-
```typescript
114-
// uploadFiles.ts
115-
const fixFilenameEncoding = (filename: string): string => {
116-
return Buffer.from(filename, 'latin1').toString('utf8')
117-
}
76+
技術問題 → 整個系統一體適用,選 Middleware ✅
11877

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?**
78+
細節分析:
12979

13080
- 這是 HTTP 上傳過程中的技術問題,不是業務邏輯
13181
- 所有檔案上傳都需要這個處理,跨多個 UseCase
13282
- 屬於請求處理層面的責任
13383

134-
### 檔案類型限制:業務邏輯 → UseCase ✅
84+
### 檔案類型限制
13585

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-
```
86+
業務邏輯 → 不同的上傳任務有不同限制,屬商業邏輯,選 UseCase ✅
14987

150-
**為什麼放 UseCase?**
88+
細節分析:
15189

15290
- 不同業務場景有不同的檔案類型限制
15391
- 這是領域知識,需要業務邏輯判斷
15492
- 可能隨著業務需求變化而調整
15593

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-
```
94+
### 這頭給你想
95+
96+
如果檔案大小限制要動態調整呢?
19597

19698
## 總結
19799

@@ -210,6 +112,4 @@ describe('UploadKnowledgeFiles', () => {
210112

211113
Clean Architecture 的精神就在於:**讓業務邏輯獨立於技術細節**
212114

213-
面試時遇到類似問題,記得從職責分離的角度思考,相信你會有不錯的表現!
214-
215-
(fin)
115+
(fin)

0 commit comments

Comments
 (0)