过去几周,我深入探讨了功能切片的构建理念,并收到大量关于代码组织结构的询问。本文将通过虚构的StoreLocation场景,揭秘我如何运用领域驱动设计(DDD)原则构建高内聚、低耦合的系统架构。这种模式可应用于地理围栏API或任何需要位置管理的领域。
核心需求:
注册商店位置,验证位置合法性(如授权区域),并通过搜索目录快速检索。
拆解为三大功能切片:
1️⃣ RegisterStoreLocation - 带业务规则的位置注册
2️⃣ GetStoreLocation - 基于ID的位置检索
3️⃣ UpdateSearchCatalog - 搜索索引实时更新
为何选择功能切片?
每个切片作为独立模块存在于有界上下文中,仅通过数据上下文或领域模型共享资源。修改商店逻辑时,只需调整对应切片,实现局部影响域。
领域组成要素:
• 领域模型(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);
// 其他字段映射...
}
}
优势解读:
• 支持多数据库策略无缝切换
• 独立管理表结构变更
• 领域模型专注业务表达
跨上下文通信机制:
StoreLocationRegistered
事件事件信封设计:
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);
}
领域测试(状态验证):
[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);
}
| 维度 | 传统分层架构 | 功能切片架构 | |-----------------|--------------|--------------| | 修改影响范围 | 跨层修改 | 单切片修改 | | 领域知识集中度 | 分散 | 高内聚 | | 测试维护成本 | 高 | 低 | | 新技术适配速度 | 慢 | 快 | | 团队协作效率 | 低 | 高 |
当店铺数据平稳入库,当搜索索引实时更新,这种架构的魅力方才显现。它既允许我们快速响应简单需求,又为复杂场景预留扩展空间。通过功能切片、有界上下文与纯净领域模型的结合,我们找到了复杂性与灵活性的黄金平衡点。
立即尝试这种架构模式,感受领域驱动设计带来的结构性蜕变。你的团队会感激这份清晰,你的系统将拥抱可持续的演进之路。