7
7
8
8
## 前情提要
9
9
10
- 最近在 Code Review 時遇到一個有趣的討論:檔案上傳功能中,檔案編碼修復應該放在 Middleware 還是 UseCase?
10
+ 最近在 Code Review 時遇到一個有趣的題目:
11
11
12
- 這個問題引發了我對 Clean Architecture 分層職責的重新思考,也讓我意識到這是一個很好的面試題目。畢竟,分層架構不只是把程式碼分資料夾這麼簡單,背後有著更深層的設計哲學。
12
+ 檔案上傳功能中,檔案編碼修復應該放在 Middleware 還是 UseCase?
13
+
14
+ 這個問題引發了我對 Clean Architecture 分層職責的重新思考,與團隊背後的設計哲學。
13
15
14
16
## 問題背景
15
17
@@ -35,71 +37,32 @@ tags:
35
37
4 . ** JWT Token 驗證** - 檢查使用者身份
36
38
5 . ** 檔案內容解析** - 提取 PDF/Word 文件內容
37
39
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
+ 以下不舉例:
96
51
97
52
- ** 跨領域技術問題** (認證、編碼、錯誤處理)
98
53
- ** HTTP 協議相關** (請求解析、回應格式)
99
54
- ** 基礎設施關注點** (日誌、監控、安全)
100
55
- ** 與業務無關的技術細節**
101
56
102
- ### UseCase 處理的是...
57
+ ### Use Case 的職責:偏向商業邏輯
58
+
59
+ 驅動核心行為 → 例:建立一筆訂單流程
60
+
61
+ 執行業務規則 → 例:檢查庫存是否足夠
62
+
63
+ 協調內外資源 → 例:呼叫付款服務並更新資料庫
64
+
65
+ 以下不舉例:
103
66
104
67
- ** 特定業務場景的規則** (不同業務有不同檔案限制)
105
68
- ** 領域知識驗證** (合約標題、內容檢查)
@@ -108,90 +71,29 @@ class UploadContractFile {
108
71
109
72
## 實際案例分析
110
73
111
- ### 檔案編碼問題:技術問題 → Middleware ✅
74
+ ### 檔案編碼問題
112
75
113
- ``` typescript
114
- // uploadFiles.ts
115
- const fixFilenameEncoding = (filename : string ): string => {
116
- return Buffer .from (filename , ' latin1' ).toString (' utf8' )
117
- }
76
+ 技術問題 → 整個系統一體適用,選 Middleware ✅
118
77
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
+ 細節分析:
129
79
130
80
- 這是 HTTP 上傳過程中的技術問題,不是業務邏輯
131
81
- 所有檔案上傳都需要這個處理,跨多個 UseCase
132
82
- 屬於請求處理層面的責任
133
83
134
- ### 檔案類型限制:業務邏輯 → UseCase ✅
84
+ ### 檔案類型限制
135
85
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 ✅
149
87
150
- ** 為什麼放 UseCase? **
88
+ 細節分析:
151
89
152
90
- 不同業務場景有不同的檔案類型限制
153
91
- 這是領域知識,需要業務邏輯判斷
154
92
- 可能隨著業務需求變化而調整
155
93
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
+ 如果檔案大小限制要動態調整呢?
195
97
196
98
## 總結
197
99
@@ -210,6 +112,4 @@ describe('UploadKnowledgeFiles', () => {
210
112
211
113
Clean Architecture 的精神就在於:** 讓業務邏輯獨立於技術細節** 。
212
114
213
- 面試時遇到類似問題,記得從職責分離的角度思考,相信你會有不錯的表現!
214
-
215
- (fin)
115
+ (fin)
0 commit comments