DDD架构如何分层

DDD(领域驱动设计)的分层架构主要是为了隔离不同职责,让代码结构更清晰,避免层与层之间的强耦合。下面用更“接地气”的方式聊聊常见的分层方式~

一、传统DDD四层架构(最常用)

就像一家公司分工,每层有自己的“岗位”,互相配合但不越界:

1. 表现层(Presentation Layer)

角色:像公司的“前台”或“客服”,只负责和用户打交道。

  • 职责
  • 接收用户输入(比如网页表单、API请求),展示结果(返回JSON数据、渲染页面)。
  • 不处理业务逻辑,只负责“传话筒”,把请求转给下一层,拿到结果再“包装”给用户。
  • 例子:比如用户在电商APP下单,表现层收到订单信息,转给应用层处理,最后返回“下单成功”提示。

2. 应用层(Application Layer)

角色:类似公司的“部门主管”,不亲自干活,但协调各部门。

  • 职责
  • 定义“用户能做什么”(比如“创建订单”“查询库存”),但不写具体逻辑。
  • 调用领域层的服务或仓储,组合多个领域操作完成一个“用户任务”。
  • 特点:逻辑简单,只做“流程控制”,比如“先查库存,再扣减,最后生成订单”。
  • 例子:用户下单时,应用层调用领域层的“库存服务”检查是否有货,再调用“订单服务”生成订单。

3. 领域层(Domain Layer)

角色:相当于公司的“核心业务部门”(比如生产、研发),公司的“灵魂”所在。

  • 职责
  • 包含领域模型(如订单、用户、库存等实体)、领域服务(复杂业务逻辑)、值对象(如金额、日期等不可变数据)。
  • 实现真正的业务规则,比如“库存不足不能下单”“订单取消后自动退款”。
  • 关键:这层是DDD的核心,不依赖任何外部组件(如数据库、HTTP框架),纯业务逻辑。
  • 例子:“库存实体”里有“扣减库存”的方法,判断库存是否足够;“订单服务”里有“取消订单”的逻辑,关联库存和支付。

4. 基础设施层(Infrastructure Layer)

角色:类似公司的“后勤部门”,提供“基础设施”支持。

  • 职责
  • 负责技术细节:比如操作数据库(仓储实现)、发送邮件/短信、调用第三方API(如支付接口)。
  • 为其他层提供通用工具(如日志、配置中心)。
  • 依赖关系:其他层可以依赖基础设施层(比如领域层需要通过仓储访问数据库),但领域层不能被其他层反向依赖。
  • 例子:用MyBatis实现“订单仓储”,从数据库读取订单数据,供领域层使用。

二、简化版分层(中小型项目常用)

如果项目没那么复杂,可能合并某些层,比如:

  • 合并应用层和领域层:把简单的业务逻辑直接写在应用层,领域层只保留实体和基础规则。
  • 基础设施层按需拆分:比如把“数据库操作”和“工具类”分开,但整体还是归为一层。

三、分层的核心原则(避坑提醒)

  1. 单向依赖:上层只能依赖下层,不能反向。比如应用层依赖领域层和基础设施层,但领域层不能依赖应用层。
  2. 领域层独立:领域层不涉及任何技术细节,这样业务逻辑可复用(比如换数据库、换框架,领域层代码不用改)。
  3. 不要跨层调用:比如表现层不能跳过应用层直接调用领域层,否则分层就没意义了。

举个“买奶茶”的栗子🌰

  • 表现层:用户在小程序点击“下单”,选择奶茶口味,提交订单。
  • 应用层:收到下单请求后,调用“领域层的库存检查”→“领域层的订单生成”→“基础设施层的支付接口”。
  • 领域层
  • 库存实体:判断“波霸珍珠剩余100份,是否足够用户下单的2份”。
  • 订单实体:生成订单号、计算总价(原价+加料费用)。
  • 基础设施层:把订单数据存到数据库,调用微信支付API完成扣款。

这样分层后,不管以后小程序换成APP(表现层变了),还是支付方式从微信换成支付宝(基础设施层变了),领域层的“库存扣减”“订单计算”逻辑都不用改,复用性拉满~

咱们用「开一家包子铺」来类比 DDD 四层架构,把每个环节对应到分层里,这样更直观好懂~

一、表现层:包子铺的「点餐窗口」

角色:直接和顾客打交道的「门面」,只负责「收单」和「给包子」,不关心怎么做包子。

  • 场景
  • 顾客来买包子,说「要 2 个肉包、1 个菜包」(接收用户请求)。
  • 店员把订单信息告诉后厨(传给应用层),同时给顾客一张取餐小票(返回交互结果)。
  • 顾客吃完后评价「包子太咸」,店员记录反馈(展示结果或收集信息)。
  • 关键点:只负责「人机交互」,不涉及「做包子」的核心逻辑。

二、应用层:包子铺的「大堂经理」

角色:不亲自做包子,但协调「点餐」到「出餐」的整个流程,相当于「流程总指挥」。

  • 职责
  • 接到店员的订单后,先判断「现在是否能做这几种包子」(比如肉包需要现蒸,得问后厨有没有面和馅料)。
  • 调用「领域层的馅料库存检查」→「领域层的包子制作」→「基础设施层的收银系统」。
  • 流程控制例子: ``` 顾客下单 → 应用层:
    1. 检查馅料库存(领域层)是否足够做肉包和菜包;
    2. 若足够,通知后厨制作(调用领域层的制作逻辑);
    3. 制作完成后,调用收银系统收款(基础设施层);
    4. 通知店员给顾客包子(返回表现层)。
      ```
  • 特点:不关心「怎么和面、调馅」(领域层细节),只关心「流程是否通顺」。

三、领域层:包子铺的「后厨核心」

角色:包子铺的「灵魂」,决定包子好不好吃、能不能做,包含所有「做包子的专业知识」。

  • 组成部分
  1. 领域实体
    • 包子配方(实体):比如肉包需要「面粉 500g、猪肉 300g、酱油 20ml」,菜包需要「面粉、青菜、香菇」等固定配比(业务规则)。
    • 馅料库存(实体):记录当前有多少面粉、猪肉、青菜,扣减库存时必须满足「肉包制作至少需要 200g 猪肉」(业务逻辑)。
  2. 领域服务
    • 制作包子(服务):根据配方调和馅料、包包子、蒸包子(复杂逻辑),比如「肉包要蒸 15 分钟,菜包蒸 10 分钟」。
    • 计算成本(服务):根据馅料用量和采购价,算出每个包子的成本(业务规则)。
  3. 值对象
    • 包子价格(不可变):肉包 3 元/个,菜包 2 元/个,一旦设定不能随意修改。
    • 制作时间(不可变):记录包子开始蒸的时间和预计出锅时间。
  • 关键点:不管用「传统蒸笼」还是「智能蒸箱」(基础设施层),「调馅、包包子」的逻辑始终不变,这就是领域层的独立性。

四、基础设施层:包子铺的「后勤保障」

角色:提供「硬件」和「外部支持」,让后厨和前台能正常运转。

  • 职责
  1. 数据存储
    • 用账本记录每天的「馅料采购量、消耗量、库存量」(类似数据库仓储),供后厨查询(领域层调用)。
    • 记录顾客订单历史(比如「周三卖出 100 个肉包」),供老板分析(应用层可能需要)。
  2. 外部工具
    • 收银机(支付接口):扫码收款,对接微信/支付宝(第三方服务)。
    • 和面机、蒸箱(技术组件):后厨用这些工具实现「揉面、蒸包子」的动作(领域层调用基础设施完成具体操作)。
  3. 通用服务
    • 闹钟(日志系统):提醒后厨「包子蒸好了,该出锅了」。
    • 温度计(监控工具):检测蒸箱温度是否达标,记录异常(基础设施提供的通用能力)。

五、分层协作的完整流程(买包子全链路)

  1. 表现层:顾客说「买包子」,店员记录订单(肉包×2,菜包×1)。
  2. 应用层
  • 调用领域层「检查馅料库存」:发现猪肉剩 400g(够做 2 个肉包需要 300g),青菜剩 500g(够做 1 个菜包需要 200g)。
  • 调用领域层「制作包子」:后厨按配方做 2 个肉包、1 个菜包,蒸好后装盘。
  • 调用基础设施层「收银机」收款:总计 3×2 + 2×1 = 8 元,顾客扫码支付。
  1. 领域层
  • 制作肉包时,严格按「猪肉+酱油+葱花」的配方调馅,包好后蒸 15 分钟(领域逻辑)。
  • 扣减库存:猪肉从 400g 减到 100g,青菜从 500g 减到 300g(实体状态变更)。
  1. 基础设施层
  • 收银机完成支付后,记录这笔订单到账本(数据库存储)。
  • 蒸箱在蒸包子时,通过温度计监控温度,确保包子熟透(技术工具支持)。

类比总结

  • 表现层:只负责「和顾客互动」,像店员的点单本和取餐窗口。
  • 应用层:只负责「流程调度」,像大堂经理安排后厨和收银。
  • 领域层:只负责「核心业务」,像后厨的配方和制作手艺,决定包子铺的竞争力。
  • 基础设施层:只负责「技术支持」,像厨房设备和账本,是支撑业务运行的基础。

通过这种分层,哪怕包子铺从「街边小店」升级成「连锁品牌」(换了收银系统或供应链),只要后厨的「配方和手艺」(领域层)不变,核心业务就能稳定运行~

那么如何将DDD四层架构应用到实际项目开发中?

将DDD四层架构应用到实际项目开发中,需要结合具体业务场景逐步落地。以下是一个可操作的实践路径,结合示例说明:

一、前期准备:理解业务边界(核心)

  1. 识别领域模型
  • 用「贫血模型」快速梳理业务概念(如电商的OrderProductInventory)。
  • 示例:某在线教育系统,核心模型可能是Course(课程)、Student(学生)、LearningRecord(学习记录)。
  1. 划分限界上下文
  • 将大系统拆分为小的「自治区域」,每个区域有独立的业务规则。
  • 示例
    • 课程管理上下文:负责课程创建、编辑、上架。
    • 学习记录上下文:记录学生学习进度、完成状态。
    • 支付上下文:处理订单、支付、退款。

二、代码结构实现(以Java为例)

1. 表现层(Presentation)

  • 职责:接收请求,返回响应,不包含业务逻辑。
  • 技术实现
  • Controller层:处理HTTP请求,参数校验,返回DTO(数据传输对象)。
  • DTO转换:将领域对象(Domain Model)转为前端友好的DTO。

示例代码

@RestController
@RequestMapping("/api/courses")
public class CourseController {
    @Autowired
    private CourseApplicationService courseService;

    @PostMapping
    public ResponseEntity<CourseDTO> createCourse(@RequestBody CourseCreationRequest request) {
        // 参数校验(可使用@Valid注解)
        CourseDTO course = courseService.createCourse(request.toCommand());
        return ResponseEntity.ok(course);
    }
}

2. 应用层(Application)

  • 职责:编排领域服务,处理事务,返回应用DTO。
  • 技术实现
  • ApplicationService:定义应用接口,调用领域服务完成业务流程。
  • 事务管理:使用@Transactional注解管理数据库事务。

示例代码

@Service
@Transactional
public class CourseApplicationService {
    @Autowired
    private CourseDomainService courseDomainService;
    @Autowired
    private CourseRepository courseRepository;

    public CourseDTO createCourse(CourseCreateCommand command) {
        // 创建领域对象
        Course course = courseDomainService.createCourse(
            command.getName(), 
            command.getTeacherId(),
            command.getMaxStudents()
        );

        // 持久化
        courseRepository.save(course);

        // 转换为DTO返回
        return CourseDTO.fromDomain(course);
    }
}

3. 领域层(Domain)

  • 职责:封装核心业务逻辑,不依赖外部组件。
  • 技术实现
  • 实体(Entity):有唯一标识,状态可变(如Course)。
  • 值对象(Value Object):不可变,描述领域中的概念(如MoneyCourseId)。
  • 领域服务(Domain Service):处理跨实体的复杂业务逻辑。
  • 仓储接口(Repository):定义数据访问契约,不涉及具体实现。

示例代码

// 实体
@Getter
public class Course {
    private CourseId id;
    private String name;
    private TeacherId teacherId;
    private int maxStudents;
    private List<StudentId> enrolledStudents;

    public void enrollStudent(StudentId studentId) {
        // 业务规则:课程人数不能超过最大值
        if (enrolledStudents.size() >= maxStudents) {
            throw new BusinessException("课程已满,无法报名");
        }
        this.enrolledStudents.add(studentId);
    }

    // 工厂方法(属于领域层)
    public static Course create(String name, TeacherId teacherId, int maxStudents) {
        // 领域规则:课程名称不能为空
        if (StringUtils.isBlank(name)) {
            throw new BusinessException("课程名称不能为空");
        }
        return new Course(
            CourseId.generate(), // 生成唯一ID
            name,
            teacherId,
            maxStudents,
            new ArrayList<>()
        );
    }
}

// 领域服务
@Service
public class CourseDomainService {
    public Course createCourse(String name, TeacherId teacherId, int maxStudents) {
        // 调用实体工厂方法
        return Course.create(name, teacherId, maxStudents);
    }
}

// 仓储接口
public interface CourseRepository {
    void save(Course course);
    Optional<Course> findById(CourseId id);
}

4. 基础设施层(Infrastructure)

  • 职责:实现仓储接口,提供技术支持(数据库、消息队列等)。
  • 技术实现
  • Repository实现:使用JPA/Hibernate等ORM框架实现数据访问。
  • 工具类:提供通用功能(如邮件发送、文件存储)。

示例代码

@Repository
public class JpaCourseRepository implements CourseRepository {
    @Autowired
    private SpringDataCourseRepository jpaRepo;

    @Override
    public void save(Course course) {
        // 将领域对象转换为JPA实体
        CourseJpaEntity entity = CourseJpaEntity.fromDomain(course);
        jpaRepo.save(entity);
    }

    @Override
    public Optional<Course> findById(CourseId id) {
        return jpaRepo.findById(id.getValue())
            .map(CourseJpaEntity::toDomain);
    }
}

三、关键落地技巧

  1. 从领域层开始设计
  • 先不考虑数据库表结构,专注于业务规则建模。
  • 示例:设计Course实体时,先定义enrollStudent()方法的业务逻辑,再考虑如何持久化。
  1. 渐进式实现
  • 第一阶段:用内存实现仓储(如Map),快速验证领域逻辑。
   public class InMemoryCourseRepository implements CourseRepository {
       private Map<CourseId, Course> courses = new ConcurrentHashMap<>();

       @Override
       public void save(Course course) {
           courses.put(course.getId(), course);
       }

       // 其他方法...
   }
  • 第二阶段:替换为数据库实现,领域层代码无需改动。
  1. 避免贫血模型
  • 错误做法:将业务逻辑放在Service层,Entity只是getter/setter(贫血模型)。
  • 正确做法:将业务逻辑封装在Entity中,如Course.enrollStudent()
  1. 处理跨边界调用
  • 场景:课程报名成功后,通知学习记录上下文更新状态。
  • 方案:使用领域事件(Domain Event)解耦。
   // 在Course实体中发布事件
   public void enrollStudent(StudentId studentId) {
       // 业务逻辑...
       DomainEventPublisher.publish(
           new StudentEnrolledEvent(this.id, studentId)
       );
   }

四、测试策略

  1. 单元测试:重点测试领域层逻辑
   @Test
   public void should_not_enroll_when_course_is_full() {
       // 给定一个已满的课程
       Course course = Course.create("Java高级", teacherId, 1);
       course.enrollStudent(studentId1);

       // 当尝试添加第二个学生时
       assertThrows(BusinessException.class, () -> {
           course.enrollStudent(studentId2);
       });
   }
  1. 集成测试:验证层与层之间的协作
   @SpringBootTest
   public class CourseApplicationServiceIntegrationTest {
       @Autowired
       private CourseApplicationService courseService;

       @Test
       public void should_create_course_successfully() {
           // 执行应用服务方法
           CourseDTO course = courseService.createCourse(
               new CourseCreationRequest("DDD实战", teacherId, 30)
           );

           // 验证结果
           assertNotNull(course.getId());
           assertEquals("DDD实战", course.getName());
       }
   }

五、常见误区与避坑指南

  1. 过度分层
  • 错误:为分层而分层,在简单场景强行套用四层架构。
  • 建议:小项目可合并应用层和领域层,后期按需拆分。
  1. 领域层依赖基础设施
  • 错误:在领域层直接使用@Autowired注入Repository。
  • 正确:通过构造函数注入仓储接口,保持领域层独立性。
   public class CourseService {
       private final CourseRepository courseRepository;

       public CourseService(CourseRepository courseRepository) {
           this.courseRepository = courseRepository;
       }
   }
  1. 忽视限界上下文
  • 错误:用一个大的User实体贯穿整个系统。
  • 正确:在不同上下文使用不同模型(如认证上下文中的AuthUser与订单上下文中的Customer)。

六、推荐学习资源

  1. 《实现领域驱动设计》(Vaughn Vernon)
  2. 《领域驱动设计模式、原理与实践》(Scott Millett)
  3. 开源项目参考:

通过以上步骤,可将DDD四层架构逐步落地到实际项目中,从核心领域逻辑出发,再向外扩展技术实现,最终构建出高内聚、低耦合的系统。

发表评论