DDD落地 - 概述

前言

DDD不是一套框架,只是一种思想,所以并没有标准的实现模式或者说是约束。
所以在实际应用中总是大同小异,这里作者会以仅自己认为合理的方式进行落地。

项目分层

一般分为4层:用户层(api)应用层(application)领域层(domain)基础设施层(infra)
各层级之间有严格的依赖关系,infra > api > application > domain

当然命名方式看个人习惯,并非一尘不变。比如作者这里把接口层命名为 api,仅仅因为它排名靠前,有更直观的自上而下的依赖关系。

可能读者比较好奇,为什么 infra 在最顶层?它不应该在最底层吗?

  • 是的,它确实应该在最底层,但是因为它比较特殊,它作为底层实现可以被任何一层依赖,这样就会导致其它层也会变得不稳定,因为依赖了细节。所以将其提到最顶层,让它依赖所有层,而不是所有层都来依赖它。不理解的读者可以看看设计原则:依赖倒转

该关系不可逆,api 不能依赖 infra
application 不能依赖 api/infra
domain不能依赖 application/api/infra

什么为不可逆呢?

  • 比如我在domain中,不会依赖到application 或者 api 或者 infra 中的任何东西,可以通过 import观察导包情况,如果依赖到了,就说明设计中出现了瑕疵。

各层职责

infra

包含项目中所有的实现,比如:bean配置,db交互实现,第三方组件交互实现等。

api

交互接口,只负责接受请求并转发给应用层。

交互规范:将所有的接口分为输入(CommandEvent)和输出(Query),即CQRS

application

业务请求处理,只会进行业务编排(不做具体的业务实现,只做业务调度)。

domain

业务实现。一般来说,领域需要保证一致,所以能破坏领域一致的行为都通过api的形式暴露出去(领域行为),在领域方法内部控制领域的一致(面向对象/高内聚)。

示例

这里举一个比较简单的例子,例如当前登录用户修改登录密码。

包结构:
在这里插入图片描述

api:

package com.platform.api;

import com.platform.application.service.UserService;
import com.platform.application.dto.command.PasswordModifyCommand;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author jinx
 */
@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {

    private final UserService service;

    @PutMapping("/password")
    public ResponseEntity<Void> modifyPassword(@RequestBody PasswordModifyCommand command){
        service.modifyPassword(command);
        return ResponseEntity.ok().build();
    }
}

application:

package com.platform.application.service;

import com.platform.application.SecurityUtil;
import com.platform.application.dto.command.PasswordModifyCommand;
import com.platform.domain.primitive.Password;
import com.platform.domain.user.User;
import com.platform.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

/**
* @author jinx
*/
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository repo;

    public void modifyPassword(PasswordModifyCommand command){
        // 获取当前用户信息
        User user = repo.findById(SecurityUtil.getUserId());

        Assert.notNull(user, "");

        user.changePassword(
                new Password(command.getOriginalPassword()),
                new Password(command.getCurrentPassword())
        );

        repo.save(user);
    }
}

domain:

package com.platform.domain.primitive;

import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * @author jinx
 */
public record Password(String value) {

    public Password {
        // password 规则校验
        Assert.isTrue(
                StringUtils.hasText(value) && value.length() > 10,
                "illegal password [%s]".formatted(value)
        );

        // 可添加加密解密业务
    }
}

package com.platform.domain.user;

import com.platform.domain.primitive.Password;
import lombok.Getter;

/**
 * @author jinx
 */
@Getter
public class User {

    private String id;
    private String username;
    private Password password;

    public User(String id, String username, Password password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }

    public void changePassword(Password origin, Password current){
        if(password.value().equals(origin.value())){
            password = current;
        }else {
            throw new IllegalArgumentException("original password not match");
        }
    }
}

infra:

package com.platform.infra.repo;

import com.platform.domain.primitive.Password;
import com.platform.domain.user.User;
import com.platform.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;

import java.util.Optional;

/**
 * @author jinx
 */
@Component
@RequiredArgsConstructor
public class DefaultUserRepository implements UserRepository {

    private final JpaUserRepository target;

    @Override
    public User findById(String id) {
        Optional<UserDo> udOptional = target.findById(id);

        if(udOptional.isPresent()){
            UserDo ud = udOptional.get();
            return new User(ud.getId(), ud.getUsername(), new Password(ud.getPassword()));
        }

        return null;
    }

    @Override
    public void save(User user) {

    }

    @Repository
    interface JpaUserRepository extends JpaRepository<UserDo, String>{
        // 需要开启内部类扫描
    }
}

总结

DDD中有很多概念,比如 充血模型,贫血模型,聚合,领域边界,防腐层,领域事件,领域服务等等,会在后续文章中一一举例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值