Trong quá trình phát triển phần mềm, đặc biệt với các hệ thống có vòng đời dài, một trong những vấn đề phổ biến nhất mà developer gặp phải là spaghetti code – code khó đọc, khó test, khó mở rộng và cực kỳ rủi ro khi thay đổi.
Chỉ một chỉnh sửa nhỏ cũng có thể gây ảnh hưởng dây chuyền đến toàn bộ hệ thống. Nguyên nhân không nằm ở ngôn ngữ hay framework, mà đến từ kiến trúc và cách tổ chức business logic.
Clean Architecture kết hợp với Domain-Driven Design (DDD) được sinh ra để giải quyết chính xác bài toán này.
Nguyên nhân gốc rễ của Spaghetti Code
Hầu hết các codebase xuống cấp đều có chung một số đặc điểm:
- Business logic bị trộn lẫn với UI hoặc API layer
- Logic phụ thuộc trực tiếp vào framework (ASP.NET, EF Core, v.v.)
- Logic phụ thuộc trực tiếp vào database implementation
- Coupling cao giữa các module
- Không thể unit test business logic độc lập
Khi dự án còn nhỏ, những vấn đề này chưa bộc lộ rõ. Nhưng khi hệ thống mở rộng, số lượng use case tăng lên, việc thay đổi hoặc onboard developer mới trở nên rất tốn kém.
👉 Vấn đề cốt lõi: không kiểm soát được sự phụ thuộc (dependency) trong hệ thống.
Clean Architecture là gì?
Clean Architecture là một mô hình kiến trúc phần mềm với mục tiêu:
Bảo vệ business logic khỏi các yếu tố dễ thay đổi như framework, UI và database
Trọng tâm của Clean Architecture là Dependency Rule.
Dependency Rule – Nguyên tắc nền tảng
Mọi dependency trong hệ thống chỉ được phép hướng vào trong.
Điều này có nghĩa là:
- Business logic không phụ thuộc vào framework
- Business logic không biết database là MySQL, SQL Server hay MongoDB
- Framework chỉ là chi tiết triển khai (implementation detail)
Kiến trúc thường được biểu diễn bằng các vòng tròn đồng tâm:
Các layer trong Clean Architecture
- Entities
- Chứa enterprise business rules
- Các object cốt lõi của domain
- Use Cases (Application Layer)
- Chứa application-specific business rules
- Điều phối luồng xử lý nghiệp vụ
- Interface Adapters
- Controllers
- DTOs
- Repository interfaces
- Frameworks & Drivers
- ASP.NET Core
- Entity Framework Core
- Database
- External services
Luồng xử lý request trong Clean Architecture
Ví dụ một HTTP request thanh toán đơn hàng:
- Controller nhận request, validate input
- Controller gọi một Use Case
- Use Case thao tác với Entity
- Use Case sử dụng repository interface
- Repository implementation lưu dữ liệu xuống database
👉 Business logic không bị rò rỉ ra ngoài controller
👉 Infrastructure chỉ là lớp triển khai, có thể thay thế
Ví dụ Clean Architecture bằng C#
1️⃣ Entity (Domain Layer)
namespace Domain.Entities;
public class Order
{
public Guid Id { get; private set; }
public OrderStatus Status { get; private set; }
public Order(Guid id)
{
Id = id;
Status = OrderStatus.New;
}
public void Pay()
{
if (Status == OrderStatus.Paid)
throw new InvalidOperationException("Order already paid.");
Status = OrderStatus.Paid;
}
}
public enum OrderStatus
{
New,
Paid
}
2️⃣ Repository Interface (Application Layer)
namespace Application.Interfaces;
using Domain.Entities;
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id);
Task SaveAsync(Order order);
}
3️⃣ Use Case (Application Layer)
namespace Application.UseCases;
using Application.Interfaces;
public class PayOrderUseCase
{
private readonly IOrderRepository _orderRepository;
public PayOrderUseCase(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task ExecuteAsync(Guid orderId)
{
var order = await _orderRepository.GetByIdAsync(orderId)
?? throw new Exception("Order not found");
order.Pay();
await _orderRepository.SaveAsync(order);
}
}
4️⃣ Repository Implementation (Infrastructure Layer)
namespace Infrastructure.Persistence;
using Application.Interfaces;
using Domain.Entities;
public class EfCoreOrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public EfCoreOrderRepository(AppDbContext context)
{
_context = context;
}
public async Task<Order?> GetByIdAsync(Guid id)
{
return await _context.Orders.FindAsync(id);
}
public async Task SaveAsync(Order order)
{
_context.Orders.Update(order);
await _context.SaveChangesAsync();
}
}
👉 Use Case không hề biết EF Core tồn tại
👉 Có thể mock IOrderRepository để unit test
Folder Structure chuẩn Clean Architecture (ASP.NET Core)
src/
│
├── Domain/
│ ├── Entities/
│ ├── ValueObjects/
│ ├── Enums/
│ └── Exceptions/
│
├── Application/
│ ├── Interfaces/
│ ├── UseCases/
│ ├── DTOs/
│ └── Validators/
│
├── Infrastructure/
│ ├── Persistence/
│ │ ├── DbContext/
│ │ └── Repositories/
│ ├── Services/
│ └── Migrations/
│
├── WebApi/
│ ├── Controllers/
│ ├── Filters/
│ ├── Middleware/
│ └── Program.cs
│
└── Tests/
├── Domain.Tests/
└── Application.Tests/
Dependency Direction
WebApi
↓
Application
↓
Domain
Infrastructure implements interfaces của Application,
nhưng không được ngược lại.
Vai trò của Domain-Driven Design (DDD)
DDD tập trung vào:
- Mô hình hoá domain đúng với nghiệp vụ
- Ngôn ngữ chung giữa dev và business (Ubiquitous Language)
- Entities, Value Objects, Aggregates
Clean Architecture cung cấp khung kỹ thuật để các khái niệm DDD:
- Không bị phá vỡ
- Không bị phụ thuộc framework
- Không bị “ăn mòn” theo thời gian
Lợi ích thực tế
Khi áp dụng Clean Architecture + DDD một cách kỷ luật:
- Business logic test được 100% bằng unit test
- Refactor an toàn hơn
- Dễ thay đổi database hoặc framework
- Codebase dễ đọc, dễ onboarding
- Phần mềm phản ánh chính xác nghiệp vụ
Kết luận
Câu hỏi quan trọng cho mỗi dự án là:
Chúng ta đang xây dựng một hệ thống chỉ để chạy được hôm nay
hay một hệ thống có thể tồn tại và mở rộng trong nhiều năm?
Clean Architecture và Domain-Driven Design không phải là giải pháp nhanh,
nhưng là đầu tư dài hạn cho chất lượng phần mềm.
Và đó chính là cách tiếp cận hiệu quả nhất để thoát khỏi spaghetti code.
Leave a comment
Your email address will not be published. Required fields are marked *