|
| 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