Clean Architecture - 整潔架構實作
📋 技能描述
Clean Architecture(整潔架構)是由 Robert C. Martin (Uncle Bob) 提出的軟體架構模式,強調:
- •依賴反轉: 內層不依賴外層,依賴方向永遠指向核心
- •關注點分離: 每層有清晰的職責邊界
- •可測試性: 商業邏輯獨立於框架、UI、資料庫
- •獨立性: 核心邏輯不受外部變更影響
🎯 使用時機
當您需要:
- •建立長期維護的企業級應用
- •確保商業邏輯可獨立測試
- •支援多種 UI (Web, Mobile, Desktop)
- •未來可能更換資料庫或第三方服務
- •團隊協作需要清晰的程式碼結構
🧩 架構層次
1. Domain Layer (領域層) - 核心
code
Domain/
├── Entities/ # 實體(商業物件)
│ ├── Product.cs
│ └── Order.cs
├── ValueObjects/ # 值物件
│ ├── Money.cs
│ └── Address.cs
├── Enums/ # 列舉
│ └── OrderStatus.cs
├── Exceptions/ # 領域例外
│ └── InsufficientStockException.cs
└── Interfaces/ # 倉儲介面(抽象)
└── IProductRepository.cs
特性:
- •零外部依賴
- •包含商業規則
- •框架無關
2. Application Layer (應用層)
code
Application/
├── Commands/ # CQRS 命令
│ └── CreateProduct/
│ ├── CreateProductCommand.cs
│ └── CreateProductCommandHandler.cs
├── Queries/ # CQRS 查詢
│ └── GetProducts/
│ ├── GetProductsQuery.cs
│ └── GetProductsQueryHandler.cs
├── DTOs/ # 資料傳輸物件
│ └── ProductDto.cs
├── Interfaces/ # 應用服務介面
│ └── IEmailService.cs
└── Validators/ # 驗證器
└── CreateProductCommandValidator.cs
特性:
- •僅依賴 Domain 層
- •協調領域物件執行業務流程
- •使用 MediatR 實作 CQRS
3. Infrastructure Layer (基礎設施層)
code
Infrastructure/
├── Persistence/ # 資料持久化
│ ├── ApplicationDbContext.cs
│ ├── Repositories/
│ │ └── ProductRepository.cs
│ └── Configurations/
│ └── ProductConfiguration.cs
├── Services/ # 外部服務實作
│ ├── EmailService.cs
│ └── StorageService.cs
└── Extensions/ # DI 擴充方法
└── ServiceCollectionExtensions.cs
特性:
- •實作 Domain 與 Application 定義的介面
- •包含 EF Core、第三方整合
- •可替換性高
4. Presentation Layer (展示層)
code
WebApi/ # API 專案 ├── Controllers/ │ └── ProductsController.cs ├── Filters/ │ └── ValidationFilter.cs ├── Middleware/ │ └── ExceptionHandlingMiddleware.cs └── Program.cs 或 BlazorApp/ # Blazor 專案 WebMvc/ # MVC 專案
特性:
- •僅依賴 Application 層
- •處理 HTTP、UI 相關邏輯
- •可同時存在多個展示層
💡 實作範例
範例 1: Domain 實體設計
csharp
/// <summary>
/// 產品實體(Domain Entity)
/// </summary>
public class Product : BaseEntity
{
// 私有建構子(強制使用工廠方法)
private Product() { }
public string Name { get; private set; } = string.Empty;
public string Description { get; private set; } = string.Empty;
public Money Price { get; private set; } = null!;
public int Stock { get; private set; }
public ProductCategory Category { get; private set; } = null!;
/// <summary>
/// 工廠方法(建立商品)
/// </summary>
public static Product Create(
string name,
string description,
Money price,
int stock,
ProductCategory category)
{
// 領域規則驗證
if (string.IsNullOrWhiteSpace(name))
throw new DomainException("商品名稱不可為空");
if (price.Amount <= 0)
throw new DomainException("商品價格必須大於0");
return new Product
{
Name = name,
Description = description,
Price = price,
Stock = stock,
Category = category
};
}
/// <summary>
/// 領域方法(減少庫存)
/// </summary>
public void ReduceStock(int quantity)
{
if (quantity <= 0)
throw new DomainException("扣除數量必須大於0");
if (Stock < quantity)
throw new InsufficientStockException(
$"商品 {Name} 庫存不足,目前: {Stock},需要: {quantity}");
Stock -= quantity;
// 領域事件
AddDomainEvent(new StockReducedEvent(Id, quantity));
}
}
/// <summary>
/// 值物件(Money)
/// </summary>
public class Money : ValueObject
{
public decimal Amount { get; private set; }
public string Currency { get; private set; } = "TWD";
public Money(decimal amount, string currency = "TWD")
{
if (amount < 0)
throw new DomainException("金額不可為負數");
Amount = amount;
Currency = currency;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
}
範例 2: Application 層 CQRS
csharp
/// <summary>
/// 建立商品命令
/// </summary>
public record CreateProductCommand(
string Name,
string Description,
decimal Price,
int Stock,
int CategoryId
) : IRequest<int>;
/// <summary>
/// 命令處理器
/// </summary>
public class CreateProductCommandHandler
: IRequestHandler<CreateProductCommand, int>
{
private readonly IProductRepository _repository;
private readonly IUnitOfWork _unitOfWork;
public CreateProductCommandHandler(
IProductRepository repository,
IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}
public async Task<int> Handle(
CreateProductCommand request,
CancellationToken cancellationToken)
{
// 建立領域物件
var category = await _repository.GetCategoryByIdAsync(request.CategoryId);
var product = Product.Create(
request.Name,
request.Description,
new Money(request.Price),
request.Stock,
category);
// 持久化
await _repository.AddAsync(product);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return product.Id;
}
}
/// <summary>
/// FluentValidation 驗證器
/// </summary>
public class CreateProductCommandValidator
: AbstractValidator<CreateProductCommand>
{
public CreateProductCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("商品名稱為必填")
.MaximumLength(200);
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("價格必須大於0");
RuleFor(x => x.Stock)
.GreaterThanOrEqualTo(0).WithMessage("庫存不可為負數");
}
}
範例 3: Infrastructure 倉儲實作
csharp
/// <summary>
/// 產品倉儲實作
/// </summary>
public class ProductRepository : IProductRepository
{
private readonly ApplicationDbContext _context;
public ProductRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<Product?> GetByIdAsync(int id)
{
return await _context.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task<IEnumerable<Product>> GetAllAsync()
{
return await _context.Products
.Include(p => p.Category)
.OrderBy(p => p.Name)
.ToListAsync();
}
public async Task AddAsync(Product product)
{
await _context.Products.AddAsync(product);
}
public void Update(Product product)
{
_context.Products.Update(product);
}
public void Delete(Product product)
{
_context.Products.Remove(product);
}
}
範例 4: API 控制器
csharp
/// <summary>
/// 產品 API 控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
/// 建立新商品
/// </summary>
[HttpPost]
[ProducesResponseType(typeof(int), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateProduct(
[FromBody] CreateProductCommand command)
{
var productId = await _mediator.Send(command);
return CreatedAtAction(
nameof(GetProduct),
new { id = productId },
productId);
}
/// <summary>
/// 取得商品詳情
/// </summary>
[HttpGet("{id}")]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetProduct(int id)
{
var query = new GetProductByIdQuery(id);
var product = await _mediator.Send(query);
return product == null
? NotFound()
: Ok(product);
}
}
✅ 最佳實踐
1. 依賴方向
code
Presentation → Application → Domain ← Infrastructure
↑
(Infrastructure 實作介面)
2. 專案結構
code
Solution/ ├── src/ │ ├── Domain/ # Class Library │ ├── Application/ # Class Library │ ├── Infrastructure/ # Class Library │ └── WebApi/ # ASP.NET Core Web API ├── tests/ │ ├── Domain.UnitTests/ │ ├── Application.UnitTests/ │ └── WebApi.IntegrationTests/ └── docs/
3. NuGet 套件建議
Domain 層: 無外部依賴
Application 層:
- •MediatR
- •FluentValidation
- •AutoMapper (可選)
Infrastructure 層:
- •Microsoft.EntityFrameworkCore
- •Microsoft.EntityFrameworkCore.SqlServer
WebApi 層:
- •Swashbuckle.AspNetCore (Swagger)
- •Serilog
4. 註冊服務 (Program.cs)
csharp
// Application 層服務
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(CreateProductCommand).Assembly));
builder.Services.AddValidatorsFromAssembly(typeof(CreateProductCommand).Assembly);
// Infrastructure 層服務
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
// 自訂中介軟體
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
❌ 常見錯誤
錯誤 1: Domain 層依賴外部套件
csharp
// ❌ 不可在 Domain 層使用 EF Core
public class Product : BaseEntity
{
[Required] // ← 這是 DataAnnotations,屬於框架依賴
public string Name { get; set; }
}
// ✅ 正確:使用領域驗證
public class Product : BaseEntity
{
private string _name = string.Empty;
public string Name
{
get => _name;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new DomainException("商品名稱不可為空");
_name = value;
}
}
}
錯誤 2: 貧血領域模型
csharp
// ❌ 貧血模型(只有屬性,沒有行為)
public class Order
{
public int Id { get; set; }
public decimal TotalAmount { get; set; }
public OrderStatus Status { get; set; }
}
// ✅ 豐富領域模型(包含商業邏輯)
public class Order
{
private readonly List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public decimal TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }
public void AddItem(Product product, int quantity)
{
if (Status != OrderStatus.Draft)
throw new DomainException("只能修改草稿狀態的訂單");
var item = new OrderItem(product, quantity);
_items.Add(item);
RecalculateTotal();
}
private void RecalculateTotal()
{
TotalAmount = _items.Sum(i => i.Subtotal);
}
}
🔗 相關技能
📚 參考資源
- •Clean Architecture (Robert C. Martin)
- •Microsoft - Clean Architecture Template
- •Ardalis - Clean Architecture
版本: 1.0.0
最後更新: 2026-02-11
維護者: BlueWhale Development Team