Clean Architecture
2026-06-18 17:05:48
# Clean Architecture
我是進入公司之後,公司剛好來了一位資深架構師,從他身上學習到了許多以前沒有接觸過的觀念。
其中對我影響最大的,就是 Clean Architecture。
關於 Clean Architecture,其實網路上已經有非常多優秀的技術文章,問 AI 也能得到完整的條列式說明。
其實我有多次嘗試寫講述 Clean Architecture,但我發現很難,僅僅透過口述 / 文章講出來,需要思考很多細節是否正確,我已經嘗試很多次,這次我想分享自己從實際開發中理解到的 Clean Architecture,我想如果有更好的視角,或是理解的又更深一層,我會再嘗試紀錄分享出來!
---
# Clean Architecture 到底在解決什麼問題?
剛開始寫程式時,我們很容易把所有東西寫在一起。
例如:
- Controller 直接查資料庫
- 商業邏輯混在 SQL 查詢裡
- Entity 直接回傳給前端
- 一個方法超過數百行
一開始開發速度很快。
但隨著需求增加,專案通常會開始出現一些問題:
- 不敢修改既有功能
- 修改需求容易影響其他功能
- 單元測試很難撰寫
- Controller 越來越肥大
- 商業邏輯散落各處
這時候你會發現,最大的問題其實不是功能難做,而是程式越來越難維護。
而 Clean Architecture 的目的,就是希望把不同職責拆分清楚,降低各層之間的耦合。
---
# 我自己的開發習慣
每個人的開發順序都不太一樣。
有些人會從 UI 開始設計。
有些人會先思考 API。
而我自己比較習慣先推演資料庫結構,再回頭思考 UI 與 API 設計。
以前公司專案的開發方式,通常是商業邏輯與資料存取完全混在一起。
例如:
```csharp
public async Task<UserResponse> GetUser(long userId)
{
var user = await _db.Users
.FirstOrDefaultAsync(x => x.Id == userId);
return new UserResponse
{
Id = user.Id,
Name = user.Name,
Password = user.Password,
LoginToken = user.LoginToken
};
}
```
這種寫法其實很常見。
但問題也很明顯:
- 商業邏輯直接依賴資料庫
- 回傳內容容易過度暴露
- 不容易撰寫測試
- 未來需求變動時容易受到影響
---
# 用 Clean Architecture 的思維重新設計
當開始使用 Clean Architecture 之後,我的思考方式也慢慢改變了。
以前是:
```text
想到需求
↓
開始寫程式
↓
邊寫邊改
```
現在則比較像:
```text
想到需求
↓
定義商業邏輯需要什麼能力
↓
定義 Repository 介面
↓
實作資料存取
↓
串接功能
```
例如:
```csharp
public interface IUserRepository
{
Task<User> GetById(long userId);
}
```
```csharp
public class UserLogic : IUserLogic
{
private readonly IUserRepository _userRepository;
public UserLogic(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<UserResponse> GetUser(long userId)
{
var user = await _userRepository.GetById(userId);
return new UserResponse
{
Id = user.Id,
Name = user.Name
};
}
}
```
這樣的設計下:
- Repository 專心處理資料存取
- Logic 專心處理商業規則
- Response 決定哪些資料可以對外暴露
職責會變得非常明確。
---
# Dependency Injection 的角色
當專案開始拆分層級之後,就會大量使用 Dependency Injection(DI)。
例如:
```text
Controller
↓
IUserLogic
↓
UserLogic
↓
IUserRepository
↓
UserRepository
```
很多人第一次接觸時,會覺得:
> 為什麼要多寫這麼多介面?
我以前也有同樣的疑問。
但實際維護大型專案之後就會發現:
介面最大的價值,不是在現在。
而是在未來需求改變的時候。
例如:
今天 UserRepository 使用 SQL Server。
未來如果改成:
- MongoDB
- Redis
- 第三方 API
理論上只需要更換實作。
上層的商業邏輯不需要修改。
這也是依賴反轉(Dependency Inversion)的核心概念之一。
---
# 我對 Repository 介面的理解
以前我一直認為:
```text
IUserRepository
應該屬於 Repository
```
但後來慢慢理解到另一種思考方式。
其實:
```text
IUserRepository
是商業邏輯需要的能力定義
```
真正擁有這個介面的,應該是 Logic。
而 Repository 只是負責實作它。
也就是說:
```text
UserLogic
定義需求
UserRepository
負責實現需求
```
這樣商業邏輯就不會依賴特定資料庫技術。
---
# 使用 Clean Architecture 之後的感受
剛開始接觸時,我覺得它很麻煩。
因為:
- 檔案變多
- 介面變多
- 開發時間變長
尤其在小專案裡,甚至會覺得有點過度設計。
但當系統規模開始變大後,就會慢慢感受到它的價值。
現在開發一個功能時,我通常會先思考:
- 商業邏輯有哪些能力?
- Repository 應該提供哪些方法?
- 哪些資料應該對外暴露?
- 哪些資料不應該被存取?
很多問題其實在設計階段就已經被發現。
等真正開始實作時,反而會順利很多。
---
# 為什麼說「寫測試變簡單了」
對我來說,Clean Architecture 最有感的好處之一,就是測試變得容易很多。
回頭看一開始那種混在一起的寫法:
```csharp
public async Task<UserResponse> GetUser(long userId)
{
var user = await _db.Users
.FirstOrDefaultAsync(x => x.Id == userId);
return new UserResponse
{
Id = user.Id,
Name = user.Name
};
}
```
如果想對這段邏輯寫單元測試,會發現很麻煩:
- 它直接依賴 `_db`
- 要測試就得準備一個真實或假的資料庫
- 還要塞測試資料進去
- 測試跑起來又慢又不穩定
換句話說,你想測的明明是「商業邏輯」,卻被「資料庫」綁在一起,根本拆不開。
而當我們把資料存取抽成 `IUserRepository` 介面之後,情況就完全不一樣了。
因為 `UserLogic` 依賴的是介面,不是真正的資料庫,所以測試時只要替換一個假的實作就好。
## 用 Mock 撰寫測試
以常見的 Moq 為例:
```csharp
[Fact]
public async Task GetUser_應該只回傳可對外的欄位()
{
// Arrange:準備一個假的 Repository
var mockRepository = new Mock<IUserRepository>();
mockRepository
.Setup(x => x.GetById(1))
.ReturnsAsync(new User
{
Id = 1,
Name = "Oyster",
Password = "should-not-expose",
LoginToken = "secret-token"
});
var logic = new UserLogic(mockRepository.Object);
// Act
var result = await logic.GetUser(1);
// Assert
Assert.Equal(1, result.Id);
Assert.Equal("Oyster", result.Name);
}
```
這個測試完全不需要:
- 連線資料庫
- 準備測試資料表
- 等待 I/O
它只專注在一件事:**給定某筆 User 資料,商業邏輯的輸出對不對。**
跑起來毫秒等級,而且每次結果都一樣。
## 不用 Mock 套件也可以
如果不想引入 Mock 套件,自己手寫一個假的實作也很簡單:
```csharp
public class FakeUserRepository : IUserRepository
{
private readonly User _user;
public FakeUserRepository(User user)
{
_user = user;
}
public Task<User> GetById(long userId)
{
return Task.FromResult(_user);
}
}
```
```csharp
[Fact]
public async Task GetUser_不應該回傳密碼與Token()
{
var repository = new FakeUserRepository(new User
{
Id = 1,
Name = "Oyster",
Password = "should-not-expose",
LoginToken = "secret-token"
});
var logic = new UserLogic(repository);
var result = await logic.GetUser(1);
// UserResponse 根本沒有 Password / LoginToken 欄位
// 從型別層面就保證了不會過度暴露
Assert.Equal("Oyster", result.Name);
}
```
這也呼應了前面提到的 Excessive Data Exposure 問題。
當回傳型別被明確設計成 `UserResponse`,加上測試替我們守住這條界線,敏感欄位就很難不小心被外洩出去。
## 真正的關鍵在「依賴介面」
其實測試會變簡單,並不是因為用了某個測試框架。
而是因為架構讓商業邏輯**依賴介面、而不是依賴具體實作**。
只要能輕鬆替換掉外部依賴,測試自然就容易寫。
這也是為什麼當初覺得「多寫那麼多介面很麻煩」,後來卻會慢慢覺得值得的原因之一。
---
# 結語
Clean Architecture 並不是為了讓專案看起來比較厲害。
它真正的目的,是讓商業邏輯不要被資料庫、框架或 UI 綁死。
當需求持續增加時,你仍然可以:
- 容易修改
- 容易測試
- 容易擴充
- 容易維護
這也是我實際使用之後,感受最深的一件事情。
或許一開始會覺得麻煩。
但當專案維護一年、兩年之後,你會很慶幸當初有把架構設計好。
使用 AI 實踐 Clean Architecture 更是一件輕鬆的事情,要注意的是,有時候我們是很難用 AI 寫出自己不知道的程式碼,或是寫出來會感到困惑,在 AI 時代下,能夠將 AI 控制的好,就是軟體工程師的專業,而能寫出符合專案設計的程式碼,也是專業技能之一
意思是說,不一定要使用特定框架、架構,單看想完成什麼事情而決定
工程師就是能避免—過度設計、設計不良,將事情做的漂亮,也是專業能力
點擊複製文章連結