洋葱架构:同心圆依赖原则

FreeGuideOnline 最新 2026-06-18

洋葱架构:构建可维护软件的核心设计模式

什么是洋葱架构?

洋葱架构(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

在洋葱架构中,数据访问不再是系统的基础,领域模型才是;数据库只是一个外部实现细节。

依赖倒置如何工作 —— 仓储模式示例

以典型的仓储模式为例,说明依赖规则如何被遵守:

  1. 领域层定义接口(内层持有抽象)
// 在 Domain 项目中
public interface IOrderRepository
{
    Order GetById(Guid orderId);
    void Save(Order order);
}
  1. 应用层消费接口(只依赖抽象)
public class OrderService
{
    private readonly IOrderRepository _repo;
    // 通过构造函数注入实现
    public OrderService(IOrderRepository repo) { _repo = repo; }
    public void CompleteOrder(Guid id) { ... }
}
  1. 基础设施层实现接口(外层实现细节)
public class SqlOrderRepository : IOrderRepository
{
    private readonly DbContext _context;
    // 使用 EF Core、SQL 语句或任何数据库
    public Order GetById(Guid id) { ... }
    public void Save(Order order) { ... }
}
  1. 依赖注入绑定
// 在启动时,将所有接口与实现绑定
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.DomainProject.Application)并严格管理引用方向。

常见误区

  • 把洋葱架构等同于 DDD:洋葱架构是实现领域驱动设计的理想架构,但不强制使用 DDD 全部概念。
  • 在领域层引入 ORM 特性:领域实体应保持纯粹,不使用任何框架标注 (如 [Table])。
  • 跨层直接访问:表现层绝不应该直接调用基础设施或领域实体,必须通过应用层。
  • 过度的抽象:无需为每个类都创建接口,只为确实需要倒置依赖的边界定义抽象。

总结

洋葱架构通过简单的同心圆依赖规则,迫使开发团队将核心业务逻辑与外部技术细节彻底分离。它带来的长期收益——可维护性、可测试性和技术灵活性——远超初期的学习曲线。对于任何希望构建健壮、长生命周期软件系统的项目而言,它都是一个经过实践检验的可靠选择。