前言
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
交互接口,只负责接受请求并转发给应用层。
交互规范:将所有的接口分为输入(
Command
,Event
)和输出(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中有很多概念,比如 充血模型,贫血模型,聚合,领域边界,防腐层,领域事件,领域服务等等,会在后续文章中一一举例。