916 字
5 分钟
结合 eShopOnWeb 全面认识领域模型架构(DDD 实战解析)
一、 架构全局观:层次依赖与职责划分
eShopOnWeb 虽然是一个单体应用,但其严格遵循了 领域驱动设计 (DDD) 的核心思想。
- Web 层 (表现层 + 应用服务层):负责处理 HTTP 请求、ViewModel 转换及缓存管理。
- ApplicationCore (领域层):系统的灵魂。包含业务实体、接口定义及业务规则,不依赖任何外部框架。
- Infrastructure (基础设施层):负责“苦力活”,如数据库访问 (EF Core)、Identity 认证、邮件发送的具体实现。
二、 Web 层:现代 ASP.NET Core 的最佳实践
eShopOnWeb 将表现层与应用逻辑整合,这在单体架构中非常高效。其核心亮点在于:
1. 健壮性保障:Health Checks
不仅检查数据库是否连通,还能检查自定义业务 API。
services.AddHealthChecks() .AddCheck<HomePageHealthCheck>("home_page_check") .AddCheck<ApiHealthCheck>("api_check");2. 装饰器模式的应用:缓存层
eShopOnWeb 优雅地利用 DI 实现了缓存逻辑的无侵入式注入。
// 通过装饰器模式,在不修改原有业务代码的情况下增加缓存功能services.AddScoped<ICatalogViewModelService, CachedCatalogViewModelService>();三、 ApplicationCore:领域驱动设计的核心
这是项目中最有价值的部分,我们通过以下四个核心组件来拆解。
1. 实体 (Entity) 与 值对象 (Value Object)
- 实体:有唯一 ID(如
BasketItem)。 - 值对象:没有 ID,由其属性定义。例如
Address。如果两个地址的街道、城市、邮编都一样,它们就是同一个地址。
最佳实践建议:在 C# 12+ 中,推荐使用
record来实现值对象,以获得原生支持的不可变性和相等性比较。
2. 聚合 (Aggregate) 与 聚合根 (IAggregateRoot)
聚合是保证业务逻辑完整性的最小单位。
- 设计原则:外部对象只能通过聚合根访问聚合内的子实体。
- 示例:
Basket(购物车)是聚合根,BasketItem则是其内部实体。
public class Basket : BaseEntity, IAggregateRoot{ private readonly List<BasketItem> _items = new(); // 关键点:只读集合,防止外部直接对 _items 进行 Add/Remove 操作 public IReadOnlyCollection<BasketItem> Items => _items.AsReadOnly();
public void AddItem(int catalogItemId, decimal unitPrice, int quantity = 1) { // 业务规则:如果商品已存在,则增加数量,而不是重复添加 if (!Items.Any(i => i.CatalogItemId == catalogItemId)) { _items.Add(new BasketItem(catalogItemId, quantity, unitPrice)); return; } var existingItem = Items.First(i => i.CatalogItemId == catalogItemId); existingItem.AddQuantity(quantity); }}3. 领域规则:规约模式 (Specification)
eShopOnWeb 避免了在 Repository 中写臃肿的查询逻辑,而是将查询条件封装成 ISpecification。
- 优点:高度复用查询逻辑,易于单元测试。
4. 领域服务 (Domain Service)
当某些逻辑跨越多个聚合,或者不属于任何特定聚合时(例如:结算订单时需要校验购物车并生成订单),应使用领域服务。
四、 Infrastructure:解耦的技术支撑
在这一层,我们实现 ApplicationCore 中定义的接口。
1. 通用仓储 (Generic Repository)
eShopOnWeb 使用了泛型仓储,配合规约模式,极大地减少了冗余代码:
// 无需为每个实体写查询逻辑,只需传入对应的 Specvar basketSpec = new BasketWithItemsSpecification(basketId);var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec);2. 依赖倒置原则 (DIP)
ApplicationCore 定义接口,Infrastructure 实现接口。这样如果你想把存储从 SQL Server 换成 CosmosDB,只需要在 Infrastructure 层增加一个新的实现,而核心业务代码无需改动。
五、 总结:eShopOnWeb 告诉我们什么?
- 保持核心纯净:
ApplicationCore不应引用EntityFramework或任何 Web 组件。 - 小聚合优于大聚合:避免设计过于臃肿的聚合,减少事务冲突。
- 显示领域意图:在实体中使用
AddItem这种业务语义明确的方法,而不是简单的Getter/Setter(这就是所谓的“充血模型”)。
结合 eShopOnWeb 全面认识领域模型架构(DDD 实战解析)
https://sw.rscclub.website/posts/eshopddd/