
分层架构核心逻辑:从理论到.NET Core落地
很多人觉得DDD是”高大上”的理论,其实它最实在的价值就是帮你把”做什么”(业务)和”怎么做”(技术)分开。就像盖房子,先画好户型图(业务模型),再考虑用什么砖(技术框架)。在.NET Core里实现DDD,分层架构是基础,这四层你得搞明白:
领域层:业务逻辑的”心脏”
这层是DDD的灵魂,专门放业务规则和领域模型。你可以把它理解成”只说业务话”的地方——比如订单怎么创建、商品能不能退款,这些规则都写在这里。我见过不少项目把业务逻辑塞在Service里,结果技术框架一换(比如从EF Core换成Dapper),业务代码跟着改,这就是没把领域层拎清楚。
在.NET Core里,领域层通常是个类库项目,里面主要有:
举个代码例子,电商订单的实体类应该这样写(重点看领域方法,把业务规则封装在里面):
// 领域层 实体
public class Order AggregateRoot
{
public Guid Id { get; private set; } // 聚合根ID
public string OrderNo { get; private set; } // 订单编号
public Address ShippingAddress { get; private set; } // 值对象:收货地址
public List Items { get; private set; } = new(); // 订单项集合
public OrderStatus Status { get; private set; } = OrderStatus.Draft; // 订单状态
// 领域方法:添加订单项(封装业务规则)
public void AddItem(Product product, int quantity)
{
if (Status != OrderStatus.Draft)
throw new DomainException("只有草稿状态的订单能添加商品");
if (quantity <= 0)
throw new DomainException("商品数量必须大于0");
Items.Add(new OrderItem(
productId: product.Id,
productName: product.Name,
unitPrice: product.Price,
quantity: quantity
));
}
}
应用层:”业务协调员”而非”业务执行者”
应用层是领域层和外部的”中间人”,负责协调领域对象完成业务流程,但不包含业务规则。比如”创建订单”这个用例,应用层会调用订单实体的AddItem方法,再调用仓储保存订单,至于”为什么只能草稿状态添加商品”这种规则,它不管——那是领域层的事。
我之前见过有人把业务逻辑全堆在ApplicationService里,结果领域层成了空壳子。其实应用层就像餐厅的前台,顾客点单(用户请求)后,它告诉后厨(领域层)做什么菜,再把做好的菜端给顾客,自己不做菜。
基础设施层:技术细节的”收容所”
数据库访问、缓存、消息队列这些技术相关的代码,都放这里。比如用EF Core实现仓储接口、用Redis缓存商品数据。关键是要依赖注入,让领域层只依赖接口,不依赖具体技术——这样以后想把SQL Server换成PostgreSQL,改基础设施层就行,不用动领域层代码。
表现层:用户交互的”窗口”
API控制器、页面这些直接和用户打交道的部分,负责接收请求、返回响应。这里要注意别把业务逻辑放进来,我见过控制器里写订单计算逻辑的,结果移动端和PC端各有一套,改起来要改两处,血的教训。
为了让你更清楚各层职责,我整理了一个.NET Core项目结构表,这是我重构项目时用的,亲测好用:
分层 | 项目类型 | 核心职责 | 依赖方向 |
---|---|---|---|
领域层 | Class Library | 业务规则、领域模型、领域服务 | 不依赖其他层 |
应用层 | Class Library | 协调领域对象、编排业务流程 | 依赖领域层 |
基础设施层 | Class Library | 数据库访问、缓存、第三方服务集成 | 依赖领域层、应用层 |
表现层 | ASP.NET Core Web API | 接收请求、返回响应、权限验证 | 依赖应用层、基础设施层 |
实战案例:电商订单系统DDD实现全流程
光说理论太空泛,咱们拿电商订单系统举例子——这是我做过三个项目都遇到的场景,用DDD实现特别合适。下面从领域模型设计到代码落地,一步一步带你走。
第一步:领域模型设计(核心中的核心)
先别急着写代码,拿张纸画业务流程图——订单从创建到支付、发货、完成,有哪些状态?涉及哪些对象?我当时和产品经理掰扯了两天,才把”订单取消后能不能恢复”、”商品下架后未支付订单怎么处理”这些规则定下来,这一步省了后面无数返工。
订单聚合根设计
:订单是聚合根,包含订单项(值对象)、收货地址(值对象),聚合根要保证内部数据一致性。比如取消订单时,所有订单项状态也要同步更新,不能有”订单已取消但订单项还在待发货”的情况。 值对象设计:收货地址(省、市、区、街道、门牌号)、金额(数值、币种)都是值对象。值对象的关键是”按值相等”,比如两个地址只要详细信息一样,就认为是同一个地址,不用像实体那样靠ID区分。在.NET里可以重写Equals方法实现:
// 值对象:地址
public class Address ValueObject
{
public string Province { get; }
public string City { get; }
public string District { get; }
public string Detail { get; }
public Address(string province, string city, string district, string detail)
{
// 校验逻辑(省略)
Province = province;
City = city;
District = district;
Detail = detail;
}
// 重写Equals:按属性值判断相等
protected override IEnumerable
{
yield return Province;
yield return City;
yield return District;
yield return Detail;
}
}
领域事件设计
:订单状态变更时(比如支付成功),要通知库存扣减、积分增加,这时候用领域事件解耦。在.NET Core里可以用MediatR库实现事件发布/订阅,领域层只定义事件,基础设施层实现事件处理:
// 领域事件:订单支付成功
public class OrderPaidEvent DomainEvent
{
public Guid OrderId { get; }
public decimal Amount { get; }
public OrderPaidEvent(Guid orderId, decimal amount)
{
OrderId = orderId;
Amount = amount;
}
}
// 订单实体中发布事件
public void Pay(decimal actualPayment)
{
Status = OrderStatus.Paid;
AddDomainEvent(new OrderPaidEvent(Id, actualPayment)); // 发布事件
}
第二步:仓储模式实现(数据持久化)
领域层定义仓储接口,基础设施层用EF Core实现。仓储接口只包含领域需要的方法,比如”根据订单号查询订单”、”保存订单”,别把”分页查询所有订单”这种技术相关的方法放进去——那是应用层的事。
// 领域层:仓储接口
public interface IOrderRepository IRepository
{
Task GetByOrderNoAsync(string orderNo);
Task ExistsByProductIdAsync(Guid productId);
}
// 基础设施层:EF Core实现
public class OrderRepository EfCoreRepository, IOrderRepository
{
private readonly DbContext _dbContext;
public OrderRepository(DbContext dbContext) base(dbContext)
{
_dbContext = dbContext;
}
public async Task GetByOrderNoAsync(string orderNo)
{
return await _dbContext.Set()
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.OrderNo == orderNo);
}
}
第三步:应用层服务编排
应用层实现”创建订单”用例:接收前端传来的商品列表、收货地址,调用订单实体的AddItem方法添加商品,校验库存(调用库存领域服务),最后调用仓储保存订单。代码示例:
// 应用层服务
public class OrderAppService IOrderAppService
{
private readonly IOrderRepository _orderRepository;
private readonly IInventoryService _inventoryService; // 领域服务
private readonly IUnitOfWork _unitOfWork;
public OrderAppService(IOrderRepository orderRepository,
IInventoryService inventoryService,
IUnitOfWork unitOfWork)
{
_orderRepository = orderRepository;
_inventoryService = inventoryService;
_unitOfWork = unitOfWork;
}
// 创建订单
public async Task CreateOrder(CreateOrderCommand command)
{
//
校验库存
foreach (var item in command.Items)
{
var hasStock = await _inventoryService.CheckStockAsync(item.ProductId, item.Quantity);
if (!hasStock)
throw new ApplicationException($"商品{item.ProductName}库存不足");
}
//
创建订单实体(调用领域方法)
var order = new Order(
orderNo: GenerateOrderNo(),
shippingAddress: new Address(command.Province, command.City, command.District, command.Detail)
);
foreach (var item in command.Items)
{
order.AddItem(new Product(item.ProductId, item.ProductName, item.Price), item.Quantity);
}
//
保存订单(事务控制)
await _orderRepository.AddAsync(order);
await _unitOfWork.SaveChangesAsync();
//
返回DTO
return new OrderDto
{
OrderId = order.Id,
OrderNo = order.OrderNo,
TotalAmount = order.GetTotalAmount()
};
}
}
第四步:依赖注入配置(.NET Core的强项)
在Program.cs里配置各层依赖注入,让应用层依赖仓储接口,基础设施层提供实现,这样领域层完全不依赖具体技术:
// 依赖注入配置
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped(sp => sp.GetRequiredService()); // 工作单元
踩坑提醒
:我第一次做的时候,把仓储实现直接放领域层了,结果换数据库时改了一堆领域代码。后来才明白——领域层只定义”要做什么”(接口),基础设施层告诉”怎么做”(实现),这才是依赖倒置的精髓。
按照这个流程做下来,你会发现业务逻辑全在领域层,应用层和表现层很薄,改需求时基本不用动核心业务代码。我那个电商项目后来加了”预售订单”功能,只新增了一个PresaleOrder实体和对应的应用服务,老代码一行没改,上线那天我准时下班了——这在以前想都不敢想。
如果你手头有.NET项目觉得难维护,不妨试试用DDD分层架构梳理一遍,先从画业务流程图开始,别怕慢,这一步走稳了,后面写代码会像开了挂一样顺。要是在领域模型设计或分层上卡住了,欢迎在评论区留言,我帮你看看怎么优化。
在.NET Core里搞DDD的四层架构,可不是随便把代码分到四个文件夹就完事了,这里面藏着个“依赖倒置”的规矩,你得把这个理顺了,后面写代码才不会乱。简单说就是:上层不用管下层具体怎么干,只要知道下层能提供什么功能就行——就像你点外卖,不用知道厨师怎么炒菜(具体实现),只要知道餐厅能做宫保鸡丁(抽象接口)就行。
具体到四层的依赖关系,你可以这么记:最核心的是领域层,它就像个“甩手掌柜”,只负责定业务规矩(比如订单怎么创建、商品能不能退款),不依赖任何其他层,谁都别想指挥它;然后是应用层,它是领域层的“小助理”,专门协调领域对象干活,但它只听领域层的,别的层想指挥它?门儿都没有;再往下是基础设施层,这层就像“技术打工仔”,给领域层和应用层提供实际干活的工具(比如用EF Core实现仓储接口、用Redis存数据),所以它得依赖领域层和应用层,知道人家需要什么工具;最外面的表现层(就是API控制器那些),它是“传话筒”,接收用户请求后,喊应用层来处理,所以它得依赖应用层和基础设施层。你看,整个依赖关系是从外往里指的,最核心的领域层被层层保护着,就像老北京四合院,中间的正房(领域层)最尊贵,其他厢房(应用层、基础设施层、表现层)都围着它转,这样业务逻辑才不会被技术代码带偏。
这种依赖设计的好处,我之前在电商项目里可是实打实尝到过甜头。那会儿我们要把数据库从SQL Server换成PostgreSQL,按以前的老写法,怕是得把Service层、Repository层翻个底朝天改代码。但用了DDD的四层架构后,我们只动了基础设施层——把EF Core的数据库上下文配置改了改,仓储实现里的连接字符串换了下,领域层和应用层的代码一行没动,三天就搞定了切换。你想啊,业务逻辑(领域层)和技术实现(基础设施层)彻底分开了,以后不管是换ORM框架(比如从EF Core换成Dapper),还是加缓存(Redis换成Memcached),都不用碰核心的业务代码,这不就省了大把返工的功夫?所以说,把依赖关系理清楚,可比多写几行代码重要多了,这才是DDD能让系统“抗造”的关键。
什么是领域驱动设计(DDD)?和传统开发相比有什么优势?
领域驱动设计(DDD)是一种以业务领域为核心,通过建模解决复杂业务逻辑的开发方法论。它强调“先理解业务,再设计系统”,将业务规则封装在领域层,避免技术实现与业务逻辑耦合。和传统开发(如三层架构直接堆砌业务)相比,DDD的优势在于:可维护性更高(业务规则集中管理,改需求时无需到处找代码)、扩展性更强(分层架构解耦,技术框架更换不影响业务代码)、团队协作更顺畅(领域模型成为业务与技术的“共同语言”,减少沟通成本)。尤其适合业务逻辑复杂的系统(如电商订单、金融交易)。
在.NET Core中实现DDD时,四层架构(领域层、应用层、基础设施层、表现层)的依赖关系是怎样的?
四层架构的依赖关系遵循“依赖倒置原则”,即上层依赖下层的抽象而非具体实现:领域层(核心业务)不依赖任何其他层;应用层(业务协调)仅依赖领域层;基础设施层(技术实现)依赖领域层和应用层(提供仓储、数据库等技术实现);表现层(用户交互)依赖应用层和基础设施层(调用应用服务处理请求)。这种依赖关系确保业务逻辑独立于技术框架,比如更换数据库时只需修改基础设施层,领域层代码无需变动。
如何在.NET Core中实现领域事件(Domain Event)?
领域事件用于解耦领域层内部或跨领域的业务流程(如订单支付后触发库存扣减)。在.NET Core中实现步骤通常是:
聚合根(Aggregate Root)的划分有什么原则?如何避免设计过大的聚合根?
聚合根划分需遵循两个核心原则:
什么样的项目适合用DDD+.NET Core实现?小项目用DDD会不会太复杂?
DDD更适合业务逻辑复杂、需求频繁变更的中大型项目(如电商、金融、ERP系统),这类项目需要长期维护,DDD的分层架构和领域模型能显著降低后期维护成本。小项目(如简单的CRUD工具、内部管理系统)若业务逻辑简单,用传统三层架构可能更高效,避免过度设计。但即便是小项目,也可借鉴DDD的“领域模型优先”思想(先梳理业务规则再写代码),避免后期业务膨胀后重构困难。.NET Core的轻量化特性(类库项目、依赖注入)能灵活适配不同规模项目,无需担心框架本身增加复杂度。