当项目变成一团乱麻时
你是否曾经打开过一个一年前编写的 C# 项目,然后惊恐地发现 任何修改都冒着破坏一切的风险?业务逻辑与 Entity Framework 紧密耦合,ASP.NET 控制器保存着数据库的秘密,而单元测试变成了与真实数据库的地狱式集成。熟悉吗?
这是缺乏架构的典型症状。好消息是:有一个经过验证的解决方案——六边形架构(也称为 端口与适配器)。这种由 Alistair Cockburn 提出的模式不仅仅是一个时髦的术语。对于 C# 的中大型项目来说,它是一种救星。让我们来了解它是如何工作的,并立即编写代码。
六边形架构的本质:核心与外围
想象一个六边形。中心是您的应用程序,包含用纯 C# 编写的干净业务逻辑,没有外部依赖。边缘是端口(接口),核心通过它们与外部世界通信。适配器是具体的实现:ASP.NET 控制器、EF Core 仓库、RabbitMQ 客户端。
主要规则:核心对外部世界一无所知。它只知道自己的端口。这使得无需更改业务逻辑即可更换数据库、用户界面或消息队列。
这对 C# 开发者有什么好处?
- 可测试性: 无需数据库、HTTP 和文件系统即可测试业务逻辑。
- 灵活性: 想将 PostgreSQL 替换为 MongoDB?只需更改适配器。
- 可读性: 代码变得像需求规范,而不是依赖关系的纠缠。
- 遵循 SOLID: 特别是依赖反转原则。
C# 项目结构
在 六边形架构 中,通常有三个主要层。对于 C#,可以方便地将它们组织为同一个解决方案中的独立项目:
MyApp.sln├── MyApp.Domain // 核心:实体、值对象、端口(接口)├── MyApp.Application // 用例(应用服务)├── MyApp.Infrastructure // 适配器:EF Core、外部 API、日志记录└── MyApp.Api // 输入适配器:ASP.NET 控制器 / Minimal API请注意:Domain 和 Application 是纯类库,没有对 Microsoft.AspNetCore 或 EntityFramework 的引用。只有 System 和 System.Threading.Tasks。
编写代码:真实示例
假设我们正在构建一个订单系统。需要创建订单并保存它。
步骤 1. 核心(Domain)
这里包含实体和 端口——定义核心如何与世界通信的接口。
// MyApp.Domain/Entities/Order.cspublic class Order{ public Guid Id { get; private set; } public string CustomerName { get; private set; } public decimal TotalAmount { get; private set; } public DateTime CreatedAt { get; private set; }
public Order(string customerName, decimal totalAmount) { Id = Guid.NewGuid(); CustomerName = customerName; TotalAmount = totalAmount; CreatedAt = DateTime.UtcNow; }}
// MyApp.Domain/Ports/IOrderRepository.cs (输出端口)public interface IOrderRepository{ Task<Order> GetByIdAsync(Guid id); Task AddAsync(Order order);}
// MyApp.Domain/Ports/IOrderNotifier.cs (输出端口——通知)public interface IOrderNotifier{ Task NotifyOrderCreatedAsync(Order order);}请注意:IOrderRepository 和 IOrderNotifier 是端口。核心定义了它 想要什么,但没有定义 如何 实现。