洋葱架构:同心圆依赖原则
洋葱架构:构建可维护软件的核心设计模式
什么是洋葱架构?
洋葱架构(Onion Architecture)是由 Jeffrey Palermo 在 2008 年提出的一种软件架构模式,它以领域模型作为核心,通过同心圆依赖规则将所有外部关注点隔离在最外层。其名称源于它的依赖关系图——从内向外层层展开,就像切开洋葱时看到的环形结构。
这种架构的根本目标是创建松耦合、可测试且技术无关的软件系统。它和传统分层架构最大的区别在于:依赖方向始终指向核心,而不是沿着某一条垂直线性递进。
核心原则:同心圆依赖规则
洋葱架构最核心的约束只有一条:
所有依赖关系必须从外环指向内环,内环对外环一无所知。
- 最内层代表核心领域知识和业务规则
- 外层实现具体的技术细节
- 没有任何一个内层的代码引用外层的类、函数、模块或命名空间
这条规则通过**依赖倒置原则(DIP)**实现:高层模块不依赖低层模块,二者都依赖抽象;抽象不依赖细节,细节依赖抽象。
架构层次全景
一个典型的洋葱架构由四个同心环组成,从内到外依次是:
[表现层] → [应用层] → [领域服务层] → [领域模型层]
但在实际实现中,常合并为三层结构,将领域服务和领域模型放在同一个核心中。这里我们以最常见的划分方式展开介绍。
1. 领域层(Domain Layer)—— 核心
- 位置:最内环
- 职责:封装所有业务规则、实体、值对象、聚合、领域事件和领域服务接口
- 特点:没有任何外部依赖,不使用任何框架,甚至不引用第三方库(纯编程语言对象)
- 示例内容:
- 实体:
Order,Product,Customer - 值对象:
Email,Money,Address - 聚合根:
Order聚合管理OrderLineItem - 仓储接口:
IOrderRepository(只定义,不实现) - 领域服务:
PricingService计算复杂折扣规则
- 实体:
// 领域层 — 纯实体,无外部依赖
class Order {
private Id id;
private CustomerId customerId;
private List<OrderLine> lines;
private OrderStatus status;
void addProduct(Product product, int quantity) {
// 业务规则:库存检查、重复商品合并等
}
}
2. 应用层(Application Layer)—— 用例编排
- 位置:领域层之外的第一层
- 职责:为外部使用者(如Web请求、后台任务)提供用例入口,协调领域对象,但不包含任何业务规则
- 特点:依赖领域层接口,通过依赖注入获得基础设施的实现
- 示例内容:
- 应用服务:
PlaceOrderService,UserRegistrationService - 命令/查询处理:
CreateOrderCommand,GetOrderByIdQuery - DTO(数据传输对象)
- 领域层接口的实现由外环注入,如
IOrderRepository
- 应用服务:
// 应用层 — 依赖 IOrderRepository 接口(在领域层定义)
class PlaceOrderHandler {
private IOrderRepository orderRepository;
private IProductRepository productRepository;
void handle(PlaceOrderCommand cmd) {
// 加载聚合
Order order = new Order(cmd.customerId);
// 调用领域方法
order.addProduct(productRepository.get(cmd.productId), cmd.quantity);
// 持久化
orderRepository.save(order);
}
}
3. 基础设施层(Infrastructure Layer)—— 技术实现
- 位置:应用层之外
- 职责:实现所有与外部资源交互的具体细节
- 特点:依赖内层的接口,实现后将实例注入到应用层
- 示例内容:
- 数据库实现:
OrderRepository使用 Entity Framework 或 JDBC - 外部API调用:
PaymentGatewayClient - 文件系统、消息队列、缓存等
- 数据库实现:
// 基础设施层 — 实现领域层定义的仓储接口
class OrderRepository implements IOrderRepository {
DatabaseContext db; // 框架对象
Order getById(OrderId id) { ... }
void save(Order order) { ... }
}
4. 表现层(Presentation Layer)—— 用户接口
- 位置:最外环
- 职责:接收用户输入并转换为应用层可理解的命令/查询,展示输出
- 特点:只直接依赖应用层,不知道基础设施或领域的具体细节
- 示例内容:
- REST API 控制器
- GraphQL 端点
- Web 页面、CLI 命令
// 表现层 — 调用应用层用例
class OrderController {
PlaceOrderHandler handler;
void placeOrder(PlaceOrderRequest request) {
PlaceOrderCommand cmd = toCommand(request);
handler.handle(cmd);
}
}
与传统分层架构的根本区别
传统三层架构(UI -> 业务逻辑层 -> 数据访问层)的依赖是自上而下的,业务逻辑层直接依赖具体的数据访问实现。这导致:
- 更换数据库或基础设施时会波及业务逻辑
- 单元测试必须模拟整个数据层,成本高昂
- 核心逻辑与技术细节紧耦合
洋葱架构颠倒所有权:定义抽象的权利属于内层,实现由外层提供。下图展示了两种依赖方向的差异:
传统分层: UI → BLL → DAL (高层依赖低层,脆弱)
洋葱架构: UI → Application → Domain ← Infrastructure
所有依赖指向 Domain
在洋葱架构中,数据访问不再是系统的基础,领域模型才是;数据库只是一个外部实现细节。
依赖倒置如何工作 —— 仓储模式示例
以典型的仓储模式为例,说明依赖规则如何被遵守:
- 领域层定义接口(内层持有抽象)
// 在 Domain 项目中
public interface IOrderRepository
{
Order GetById(Guid orderId);
void Save(Order order);
}
- 应用层消费接口(只依赖抽象)
public class OrderService
{
private readonly IOrderRepository _repo;
// 通过构造函数注入实现
public OrderService(IOrderRepository repo) { _repo = repo; }
public void CompleteOrder(Guid id) { ... }
}
- 基础设施层实现接口(外层实现细节)
public class SqlOrderRepository : IOrderRepository
{
private readonly DbContext _context;
// 使用 EF Core、SQL 语句或任何数据库
public Order GetById(Guid id) { ... }
public void Save(Order order) { ... }
}
- 依赖注入绑定
// 在启动时,将所有接口与实现绑定
services.AddScoped<IOrderRepository, SqlOrderRepository>();
此时,从内到外的依赖链为:Domain (IOrderRepository) ← Application ← Infrastructure (SqlOrderRepository),所有编译期箭头都指向核心,运行时由容器向外寻找实现。
洋葱架构的突出优势
- 无与伦比的测试性:核心业务逻辑无框架、无数据库依赖,可用纯单元测试覆盖。
- 持久化无关性:更改数据库或存储方式时,只需重新实现一组接口,核心应用零改动。
- 技术选择的自由:可以推迟决定使用哪种数据库、UI框架或消息系统,甚至并行开发。
- 关注点分离:职责边界清晰,新成员可以快速理解代码结构。
- 适应微服务:洋葱架构本身的模块边界很适合作为服务边界的基础。
一个真实的用例:用户注册命令
下面用一个最简化的示例展示各层如何配合完成一个“用户注册”用例。
领域层
// 值对象
public class Email {
private String value;
public Email(String value) {
if (!value.contains("@")) throw new IllegalArgumentException();
this.value = value;
}
}
// 聚合根
public class User {
private UserId id;
private Email email;
private Password password;
public User(Email email, Password password) {
// 业务规则:密码强度、邮箱唯一性(通过应用层控制)
this.email = email;
this.password = password;
}
}
// 领域层 — 仓储接口
public interface UserRepository {
User findByEmail(Email email);
void save(User user);
}
应用层
public class RegisterUserService {
private UserRepository userRepository;
public RegisterUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void register(String emailStr, String rawPassword) {
Email email = new Email(emailStr);
Password password = Password.createFromRaw(rawPassword);
// 业务规则:邮箱不能重复
if (userRepository.findByEmail(email) != null) {
throw new DuplicateEmailException();
}
User user = new User(email, password);
userRepository.save(user);
}
}
基础设施层
public class JpaUserRepository implements UserRepository {
private EntityManager em;
// ...
public User findByEmail(Email email) { /* JPQL 查询 */ }
public void save(User user) { em.persist(user); }
}
表现层
@RestController
public class RegistrationController {
private RegisterUserService service;
@PostMapping("/users")
public void register(@RequestBody RegisterRequest req) {
service.register(req.getEmail(), req.getPassword());
}
}
通过在组合根(Composition Root)将 JpaUserRepository 绑定到 UserRepository,整个流程完全符合洋葱架构的依赖规则。
最佳实践与常见误区
最佳实践
- 将接口定义在靠近使用方的地方:如果某个接口仅被应用层使用,就定义在应用层;若被领域层消费,则放在领域层。
- 避免“领域对象贫血”:领域实体应包含行为,而不是纯数据容器。
- 依赖注入是必需品:没有它,依赖倒置无法落地。
- 分层可由项目结构体现:使用多个程序集(如
Project.Domain、Project.Application)并严格管理引用方向。
常见误区
- 把洋葱架构等同于 DDD:洋葱架构是实现领域驱动设计的理想架构,但不强制使用 DDD 全部概念。
- 在领域层引入 ORM 特性:领域实体应保持纯粹,不使用任何框架标注 (如
[Table])。 - 跨层直接访问:表现层绝不应该直接调用基础设施或领域实体,必须通过应用层。
- 过度的抽象:无需为每个类都创建接口,只为确实需要倒置依赖的边界定义抽象。
总结
洋葱架构通过简单的同心圆依赖规则,迫使开发团队将核心业务逻辑与外部技术细节彻底分离。它带来的长期收益——可维护性、可测试性和技术灵活性——远超初期的学习曲线。对于任何希望构建健壮、长生命周期软件系统的项目而言,它都是一个经过实践检验的可靠选择。