.NET功能切片架构实战:领域驱动设计与高效维护的终极指南

作者:微信公众号:【架构师老卢】
3-18 9:16
8

开篇:从混沌到秩序

过去几周,我深入探讨了功能切片的构建理念,并收到大量关于代码组织结构的询问。本文将通过虚构的StoreLocation场景,揭秘我如何运用领域驱动设计(DDD)原则构建高内聚、低耦合的系统架构。这种模式可应用于地理围栏API或任何需要位置管理的领域。


🚀 需求拆解与功能切片

核心需求:
注册商店位置,验证位置合法性(如授权区域),并通过搜索目录快速检索。

拆解为三大功能切片:
1️⃣ RegisterStoreLocation - 带业务规则的位置注册
2️⃣ GetStoreLocation - 基于ID的位置检索
3️⃣ UpdateSearchCatalog - 搜索索引实时更新

为何选择功能切片?
每个切片作为独立模块存在于有界上下文中,仅通过数据上下文或领域模型共享资源。修改商店逻辑时,只需调整对应切片,实现局部影响域


🧱 有界上下文:Stores领域

领域组成要素:
• 领域模型(Store类、业务规则、领域异常等)
• 功能切片(注册/获取位置等核心操作)

项目结构全景:

src/
├── Stores/                           # 商店有界上下文
│   ├── Features/                     # 功能切片目录
│   │   ├── RegisterStoreLocation/    # 注册功能切片
│   │   ├── GetStoreLocation/         # 检索功能切片
│   └── Data/                         # 数据层
│       ├── Types/                    # 领域模型
│       │   └── Store.cs              # 纯净数据类
│       ├── Configuration/            # EF配置
│       └── StoreLocationDbContext.cs # 数据库上下文
│
└── SearchCatalog/                    # 搜索有界上下文
    ├── Features/                     
    │   └── UpdateStoreSearch/        # 事件处理切片
    └── Data/                         # 搜索优化模型

💎 纯净领域模型实践

Store类设计原则:
• 零持久化元数据
• 无ORM依赖
• 专注业务属性

public sealed class Store
{
   public Guid Id { get; init; }
   public string Name { get; set; }
   public double Latitude { get; set; }
   public double Longitude { get; set; }
   public DateTime CreatedAt { get; init; }
   public DateTime? LastUpdated { get; set; }
}

EF Core配置解耦:

public class StoreConfiguration : IEntityTypeConfiguration<Store>
{
    public void Configure(EntityTypeBuilder<Store> builder)
    {
        builder.ToTable("stores");
        builder.HasKey(t => t.Id);
        builder.Property(x => x.Id).HasColumnName("id");
        builder.Property(x => x.Name).HasMaxLength(100);
        // 其他字段映射...
    }
}

优势解读:
• 支持多数据库策略无缝切换
• 独立管理表结构变更
• 领域模型专注业务表达


⚡ 事件驱动架构实战

跨上下文通信机制:

  1. Stores上下文注册店铺时发布StoreLocationRegistered事件
  2. SearchCatalog上下文订阅事件更新Elasticsearch索引

事件信封设计:

public class EventEnvelope<T>
{
    public string EventType { get; init; }
    public int EventVersion { get; init; }
    public DateTime Timestamp { get; init; }
    public Guid CorrelationId { get; init; }
    public T Data { get; init; }
}

// 店铺注册事件数据
public record StoreLocationRegistered
{
    public Guid StoreId { get; init; }
    public string StoreName { get; init; }
    public double Latitude { get; init; }
    public double Longitude { get; init; }
}

Outbox事务模式:

// 在事务中原子化保存店铺与事件
async Task EnqueueEventFunc(EventEnvelope<StoreLocationRegistered> envelope, CancellationToken token)
{
    var outbox = new OutboxEvent
    {
        Id = Guid.NewGuid(),
        EventType = envelope.EventType,
        Payload = JsonSerializer.Serialize(envelope),
        CreatedAt = DateTime.UtcNow
    };

    dbContext.OutboxEvents.Add(outbox);
    await dbContext.SaveChangesAsync(token);
}

🛡️ 分层验证体系

边界输入验证(FastEndpoints实现):

public class RegisterStoreRequestValidator : Validator<RegisterStoreRequest>
{
    public RegisterStoreRequestValidator()
    {
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Latitude).InclusiveBetween(-90, 90);
        RuleFor(x => x.Longitude).InclusiveBetween(-180, 180);
    }
}

领域规则守卫:

private static void GuardStoreCreationLocation(RegisterStoreRequest request, RegionPolicy policy)
{
    if (!policy.Contains(request.Latitude, request.Longitude))
        throw new OutOfLicensedRegionException(request.Latitude, request.Longitude);
}

🧪 芝加哥学派TDD实践

领域测试(状态验证):

[Fact]
public async Task RegistersStore_AndPublishesEvent_WhenRequestIsValid()
{
    // 构造模拟持久化与事件发布
    var stores = new List<Store>();
    var events = new List<EventEnvelope<StoreLocationRegistered>>();

    var handler = new RegisterStoreLocationHandler(
        persistStore: (store, _) => { stores.Add(store); return Task.CompletedTask; },
        enqueueEvent: (evt, _) => { events.Add(evt); return Task.CompletedTask; },
        policy: new RegionPolicy(...)
    );

    // 执行注册
    var store = await handler.RegisterAsync(new RegisterStoreRequest(...));

    // 验证状态
    Assert.Single(stores);
    Assert.Single(events);
    Assert.Equal("StoreLocationRegistered", events[0].EventType);
}

集成测试示例:

[Fact]
public async Task Post_Stores_ReturnsBadRequest_When_StoreNameIsEmpty()
{
    var client = _factory.CreateClient();
    var response = await client.PostAsJsonAsync("/v1/stores", new RegisterStoreRequest("", 40, -3));
    
    Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

🚢 架构演进策略

  1. 简单起步:初期采用直接持久化,避免过度设计
  2. 按需事件驱动:仅在跨上下文协作时引入事件机制
  3. 技术选型自由
    • Stores上下文采用SQL实现ACID
    • SearchCatalog使用Elasticsearch优化搜索
  4. 统一事务管理:通过Outbox模式保证最终一致性

架构收益矩阵

| 维度 | 传统分层架构 | 功能切片架构 | |-----------------|--------------|--------------| | 修改影响范围 | 跨层修改 | 单切片修改 | | 领域知识集中度 | 分散 | 高内聚 | | 测试维护成本 | 高 | 低 | | 新技术适配速度 | 慢 | 快 | | 团队协作效率 | 低 | 高 |


简约而不简单

当店铺数据平稳入库,当搜索索引实时更新,这种架构的魅力方才显现。它既允许我们快速响应简单需求,又为复杂场景预留扩展空间。通过功能切片、有界上下文与纯净领域模型的结合,我们找到了复杂性与灵活性的黄金平衡点。

立即尝试这种架构模式,感受领域驱动设计带来的结构性蜕变。你的团队会感激这份清晰,你的系统将拥抱可持续的演进之路。

相关留言评论
昵称:
邮箱:
阅读排行