🏗️ Clean Architecture 與專案架構
📌 為什麼需要好的架構?
好的架構就像蓋房子的設計藍圖,沒有藍圖就動工,蓋出來的房子可能牆歪、水管漏、電線亂接。軟體也一樣!
好的架構讓你的程式碼:容易理解、容易測試、容易擴充、容易維護。
🔷 傳統三層式架構(Layered Architecture)
💡 比喻
就像一棟三層樓的辦公大樓:
- 1 樓(Presentation):接待大廳,負責接待訪客(使用者)
- 2 樓(Business Logic):辦公區,負責處理業務
- 3 樓(Data Access):檔案室,負責存取資料
📝 結構
┌─────────────────────────┐
│ Presentation Layer │ ← Controller、View(面對使用者)
├─────────────────────────┤
│ Business Logic Layer │ ← Service、商業規則(核心邏輯)
├─────────────────────────┤
│ Data Access Layer │ ← Repository、EF Core(存取資料庫)
└─────────────────────────┘
📝 C# 範例
// === Data Access Layer(資料存取層)===
// 負責跟資料庫溝通
public class ProductRepository
{
private readonly AppDbContext _context; // 資料庫上下文
public ProductRepository(AppDbContext context)
{
_context = context; // 注入資料庫上下文
}
// 根據 ID 取得產品
public async Task<Product?> GetByIdAsync(int id)
{
return await _context.Products.FindAsync(id); // 從資料庫查詢
}
// 取得所有產品
public async Task<List<Product>> GetAllAsync()
{
return await _context.Products.ToListAsync(); // 回傳所有產品
}
}
// === Business Logic Layer(商業邏輯層)===
// 負責處理商業規則
public class ProductService
{
private readonly ProductRepository _repository; // 依賴資料存取層
public ProductService(ProductRepository repository)
{
_repository = repository; // 注入 Repository
}
// 取得產品,加上商業規則(例如計算折扣)
public async Task<ProductDto?> GetProductAsync(int id)
{
var product = await _repository.GetByIdAsync(id); // 從 Repository 取資料
if (product == null) return null; // 找不到就回傳 null
return new ProductDto // 轉換成 DTO 回傳
{
Id = product.Id, // 產品 ID
Name = product.Name, // 產品名稱
Price = product.Price, // 原價
FinalPrice = product.Price * 0.9m // 商業邏輯:9 折優惠
};
}
}
// === Presentation Layer(呈現層)===
// 負責接收請求和回傳結果
// [ApiController]
// public class ProductController : ControllerBase
// {
// private readonly ProductService _service; // 依賴商業邏輯層
//
// public ProductController(ProductService service)
// {
// _service = service; // 注入 Service
// }
//
// [HttpGet("{id}")]
// public async Task<IActionResult> Get(int id)
// {
// var product = await _service.GetProductAsync(id); // 呼叫 Service
// if (product == null) return NotFound(); // 找不到回傳 404
// return Ok(product); // 回傳產品資料
// }
// }
⚠️ 三層式的問題
三層式架構的依賴方向是:Presentation → Business → Data Access。這表示商業邏輯依賴資料存取層。如果要換資料庫,商業邏輯也要跟著改!
🔷 Clean Architecture(整潔架構 / 洋蔥模型)
💡 比喻
想像一顆洋蔥:最核心的那一層是最重要的商業邏輯,外面一層一層包裹著基礎設施。核心不依賴外層,外層依賴核心!
📝 四層結構
┌──────────────────────────────┐
│ Presentation │ ← API Controller、Blazor 頁面
│ (最外層:面對使用者) │
├──────────────────────────────┤
│ Infrastructure │ ← EF Core、外部 API、檔案系統
│ (外層:技術實作細節) │
├──────────────────────────────┤
│ Application │ ← Use Case、DTO、介面定義
│ (中層:應用程式邏輯) │
├──────────────────────────────┤
│ Domain │ ← Entity、Value Object、商業規則
│ (核心:最重要的商業邏輯) │
└──────────────────────────────┘
🔑 核心原則:依賴方向由外向內
Presentation → Application → Domain ← Infrastructure
↑ │
└────────────────────────┘
Infrastructure 實作 Application 定義的介面
📝 各層的 C# 範例
// ============================================================
// 🟡 Domain Layer(領域層)— 最核心,不依賴任何其他層
// ============================================================
// 領域實體:訂單
public class Order
{
public int Id { get; private set; } // 訂單 ID
public string CustomerName { get; private set; } = ""; // 客戶名稱
public List<OrderItem> Items { get; private set; } = []; // 訂單項目清單
public DateTime CreatedAt { get; private set; } // 建立時間
public OrderStatus Status { get; private set; } // 訂單狀態
// 建構子:建立訂單時必須有客戶名稱
public Order(string customerName)
{
CustomerName = customerName; // 設定客戶名稱
CreatedAt = DateTime.UtcNow; // 記錄建立時間
Status = OrderStatus.Pending; // 預設狀態為「待處理」
}
// 商業邏輯:新增訂單項目
public void AddItem(string productName, decimal price, int quantity)
{
if (quantity <= 0) // 數量必須大於 0
throw new ArgumentException("數量必須大於零"); // 違反規則就丟例外
Items.Add(new OrderItem(productName, price, quantity)); // 加入項目
}
// 商業邏輯:計算訂單總金額
public decimal GetTotal()
{
return Items.Sum(item => item.Price * item.Quantity); // 加總所有項目
}
// 商業邏輯:確認訂單
public void Confirm()
{
if (Items.Count == 0) // 沒有項目不能確認
throw new InvalidOperationException("空訂單無法確認"); // 丟例外
Status = OrderStatus.Confirmed; // 改為已確認
}
}
// 值物件:訂單項目
public class OrderItem
{
public string ProductName { get; } // 產品名稱
public decimal Price { get; } // 單價
public int Quantity { get; } // 數量
public OrderItem(string productName, decimal price, int quantity)
{
ProductName = productName; // 設定產品名稱
Price = price; // 設定單價
Quantity = quantity; // 設定數量
}
}
// 列舉:訂單狀態
public enum OrderStatus
{
Pending, // 待處理
Confirmed, // 已確認
Shipped, // 已出貨
Delivered, // 已送達
Cancelled // 已取消
}
// ============================================================
// 🟢 Application Layer(應用層)— 定義介面和使用案例
// ============================================================
// 定義倉儲介面(在 Application 層定義,在 Infrastructure 層實作)
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(int id); // 根據 ID 查詢訂單
Task<List<Order>> GetAllAsync(); // 查詢所有訂單
Task AddAsync(Order order); // 新增訂單
Task SaveChangesAsync(); // 儲存變更
}
// 定義通知服務介面
public interface INotificationService
{
Task SendOrderConfirmationAsync( // 發送訂單確認通知
string customerName, int orderId);
}
// DTO(資料傳輸物件):用來回傳給外層
public class OrderDto
{
public int Id { get; set; } // 訂單 ID
public string CustomerName { get; set; } = ""; // 客戶名稱
public decimal Total { get; set; } // 訂單總金額
public string Status { get; set; } = ""; // 訂單狀態
}
// Use Case(使用案例):建立訂單的流程
public class CreateOrderUseCase
{
private readonly IOrderRepository _repository; // 倉儲介面
private readonly INotificationService _notification; // 通知服務介面
// 注入介面,不是具體實作!
public CreateOrderUseCase(
IOrderRepository repository,
INotificationService notification)
{
_repository = repository; // 注入倉儲
_notification = notification; // 注入通知服務
}
// 執行建立訂單的流程
public async Task<OrderDto> ExecuteAsync(
string customerName, List<(string Name, decimal Price, int Qty)> items)
{
var order = new Order(customerName); // 建立新訂單
foreach (var item in items) // 逐一加入訂單項目
{
order.AddItem(item.Name, item.Price, item.Qty); // 加入項目
}
order.Confirm(); // 確認訂單(商業規則檢查)
await _repository.AddAsync(order); // 儲存訂單
await _repository.SaveChangesAsync(); // 寫入資料庫
await _notification.SendOrderConfirmationAsync( // 發送通知
customerName, order.Id);
return new OrderDto // 回傳 DTO
{
Id = order.Id, // 訂單 ID
CustomerName = order.CustomerName, // 客戶名稱
Total = order.GetTotal(), // 訂單總金額
Status = order.Status.ToString() // 訂單狀態
};
}
}
🔷 CQRS 概念(Command Query Responsibility Segregation)
💡 比喻
就像銀行的存款窗口和查詢窗口分開:存款(寫入)走一個流程,查餘額(讀取)走另一個流程。讀寫分離,各自優化!
📝 C# 概念範例
// === Command(命令):負責寫入操作 ===
// 建立訂單的命令
public class CreateOrderCommand
{
public string CustomerName { get; set; } = ""; // 客戶名稱
public List<OrderItemDto> Items { get; set; } = []; // 訂單項目
}
// 命令處理器:處理建立訂單的邏輯
public class CreateOrderHandler
{
private readonly IOrderRepository _repository; // 寫入用的倉儲
public CreateOrderHandler(IOrderRepository repository)
{
_repository = repository; // 注入倉儲
}
public async Task<int> HandleAsync(CreateOrderCommand command)
{
var order = new Order(command.CustomerName); // 建立訂單
// ... 加入項目並儲存 ...
await _repository.AddAsync(order); // 寫入資料庫
await _repository.SaveChangesAsync(); // 儲存變更
return order.Id; // 回傳訂單 ID
}
}
// === Query(查詢):負責讀取操作 ===
// 查詢訂單的請求
public class GetOrderQuery
{
public int OrderId { get; set; } // 要查詢的訂單 ID
}
// 查詢處理器:處理查詢訂單的邏輯
public class GetOrderHandler
{
private readonly IOrderRepository _repository; // 讀取用的倉儲
public GetOrderHandler(IOrderRepository repository)
{
_repository = repository; // 注入倉儲
}
public async Task<OrderDto?> HandleAsync(GetOrderQuery query)
{
var order = await _repository.GetByIdAsync(query.OrderId); // 查詢訂單
if (order == null) return null; // 找不到回傳 null
return new OrderDto // 轉成 DTO 回傳
{
Id = order.Id, // 訂單 ID
CustomerName = order.CustomerName, // 客戶名稱
Total = order.GetTotal(), // 總金額
Status = order.Status.ToString() // 狀態
};
}
}
🔷 Repository + Unit of Work
💡 比喻
Repository 像各科的老師,每個老師管一科(一個資料表)。Unit of Work 像校長,負責統一說「大家一起交成績單(SaveChanges)」,確保所有操作要嘛全部成功、要嘛全部失敗。
📝 C# 實作
// Unit of Work 介面:統一管理所有 Repository 的交易
public interface IUnitOfWork : IDisposable
{
IOrderRepository Orders { get; } // 訂單倉儲
IProductRepository Products { get; } // 產品倉儲
Task<int> SaveChangesAsync(); // 統一儲存所有變更
}
// 使用方式
public class OrderService
{
private readonly IUnitOfWork _unitOfWork; // Unit of Work
public OrderService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork; // 注入 Unit of Work
}
public async Task CreateOrderAsync(string customerName, int productId)
{
var product = await _unitOfWork.Products // 查詢產品
.GetByIdAsync(productId);
if (product == null) // 產品不存在
throw new Exception("產品不存在"); // 丟例外
var order = new Order(customerName); // 建立訂單
order.AddItem(product.Name, product.Price, 1); // 加入產品
await _unitOfWork.Orders.AddAsync(order); // 新增訂單
// 統一儲存:訂單和產品的變更一起成功或一起失敗
await _unitOfWork.SaveChangesAsync(); // 一次性寫入資料庫
}
}
🔷 實際專案資料夾結構範例
MyProject/ # 方案根目錄
├── MyProject.sln # 方案檔
├── src/ # 原始碼目錄
│ ├── MyProject.Domain/ # 🟡 領域層
│ │ ├── Entities/ # 實體類別
│ │ │ ├── Order.cs # 訂單實體
│ │ │ └── Product.cs # 產品實體
│ │ ├── ValueObjects/ # 值物件
│ │ │ └── Money.cs # 金額值物件
│ │ ├── Enums/ # 列舉
│ │ │ └── OrderStatus.cs # 訂單狀態
│ │ └── Interfaces/ # 領域介面
│ │ └── IDomainEvent.cs # 領域事件介面
│ ├── MyProject.Application/ # 🟢 應用層
│ │ ├── DTOs/ # 資料傳輸物件
│ │ │ ├── OrderDto.cs # 訂單 DTO
│ │ │ └── ProductDto.cs # 產品 DTO
│ │ ├── Interfaces/ # 倉儲及服務介面
│ │ │ ├── IOrderRepository.cs # 訂單倉儲介面
│ │ │ └── INotificationService.cs# 通知服務介面
│ │ ├── UseCases/ # 使用案例
│ │ │ ├── CreateOrderUseCase.cs # 建立訂單
│ │ │ └── GetOrderUseCase.cs # 查詢訂單
│ │ └── Mappings/ # 物件對應設定
│ │ └── OrderProfile.cs # 訂單的 AutoMapper 設定
│ ├── MyProject.Infrastructure/ # 🔵 基礎設施層
│ │ ├── Data/ # 資料庫相關
│ │ │ ├── AppDbContext.cs # EF Core DbContext
│ │ │ └── Migrations/ # 資料庫遷移
│ │ ├── Repositories/ # 倉儲實作
│ │ │ └── OrderRepository.cs # 訂單倉儲實作
│ │ └── Services/ # 外部服務實作
│ │ └── EmailService.cs # Email 服務實作
│ └── MyProject.WebApi/ # 🔴 呈現層
│ ├── Controllers/ # API 控制器
│ │ └── OrderController.cs # 訂單 API
│ ├── Program.cs # 應用程式進入點
│ └── appsettings.json # 設定檔
└── tests/ # 測試目錄
├── MyProject.UnitTests/ # 單元測試
└── MyProject.IntegrationTests/ # 整合測試
🤔 我這樣寫為什麼會錯?
錯誤一:把商業邏輯放在 Controller 裡
// ❌ Controller 裡面寫了一堆商業邏輯
// [HttpPost]
// public async Task<IActionResult> CreateOrder(OrderRequest request)
// {
// if (request.Items.Count == 0) // 商業規則不該在這裡!
// return BadRequest("訂單不能為空");
// var total = request.Items.Sum(i => i.Price * i.Qty); // 計算邏輯也不該在這裡!
// if (total > 10000) // 折扣邏輯更不該在這裡!
// total *= 0.9m;
// // ... 存資料庫 ...
// }
// ✅ Controller 只負責接收請求和回傳結果
// ✅ 商業邏輯放在 Domain 或 Application 層
錯誤二:Domain 層依賴 Infrastructure 層
// ❌ Domain 實體直接使用 EF Core — 依賴方向錯了!
using Microsoft.EntityFrameworkCore; // Domain 層不應該引用這個!
public class Order
{
public void Save(AppDbContext context) // Domain 不應該知道 DbContext
{
context.Orders.Add(this); // 這是 Infrastructure 的工作!
context.SaveChanges(); // Domain 不該碰資料庫!
}
}
// ✅ Domain 層完全不知道資料庫的存在
// ✅ 在 Application 層定義 IOrderRepository 介面
// ✅ 在 Infrastructure 層實作 IOrderRepository
錯誤三:所有程式碼都塞在同一個專案裡
// ❌ 一個專案包含所有東西
MyProject/
├── Controllers/ # 呈現邏輯
├── Models/ # 混合了 Entity 和 DTO
├── Services/ # 混合了商業邏輯和資料存取
└── Data/ # 資料庫相關
// 結果:改一個功能要翻遍整個專案,測試也很難寫
// ✅ 依照 Clean Architecture 分成多個專案
// 每層各自獨立,依賴方向清楚,容易維護和測試
📝 架構選擇指南
| 專案規模 | 推薦架構 | 理由 |
|---|---|---|
| 小型專案(PoC、工具) | 單層或三層式 | 快速開發,不過度設計 |
| 中型專案(企業內部系統) | 三層式 + Repository | 結構清楚又不會太複雜 |
| 大型專案(商業產品) | Clean Architecture | 高度解耦,易於測試和維護 |
| 高流量系統 | Clean Architecture + CQRS | 讀寫分離,各自優化效能 |
💡 記住:沒有最好的架構,只有最適合的架構。不要為了一個小工具套用 Clean Architecture,也不要用三層式架構去做大型商業系統!