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 使用了泛型仓储,配合规约模式,极大地减少了冗余代码:

// 无需为每个实体写查询逻辑,只需传入对应的 Spec
var basketSpec = new BasketWithItemsSpecification(basketId);
var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec);

2. 依赖倒置原则 (DIP)#

ApplicationCore 定义接口,Infrastructure 实现接口。这样如果你想把存储从 SQL Server 换成 CosmosDB,只需要在 Infrastructure 层增加一个新的实现,而核心业务代码无需改动。


五、 总结:eShopOnWeb 告诉我们什么?#

  1. 保持核心纯净ApplicationCore 不应引用 EntityFramework 或任何 Web 组件。
  2. 小聚合优于大聚合:避免设计过于臃肿的聚合,减少事务冲突。
  3. 显示领域意图:在实体中使用 AddItem 这种业务语义明确的方法,而不是简单的 Getter/Setter(这就是所谓的“充血模型”)。
结合 eShopOnWeb 全面认识领域模型架构(DDD 实战解析)
https://sw.rscclub.website/posts/eshopddd/
作者
杨月昌
发布于
2023-05-18
许可协议
CC BY-NC-SA 4.0