Gradle多模块项目六边形架构:应用层输出转换方案咨询
这是个非常典型的六边形架构下的封装与适配问题,我刚好有几个成熟的方案可以帮你解决,完全符合你不暴露领域模型、让客户端控制输出的需求:
方案1:基于输出端口的泛型转换(贴合六边形架构设计)
在六边形架构中,应用层的核心职责之一是定义输出端口(Output Port),外部适配器(比如你的rest-adapter)通过实现这些端口来完成结果的转换。这里的关键是让输出端口成为泛型接口,且完全不依赖领域模型——应用层会主动提取领域对象的必要字段,传递给端口实现。
具体步骤:
- 在
application模块(应用层)定义泛型输出端口接口:public interface ResultConverter<T> { // 这里只暴露领域模型需要对外提供的字段,用原始类型或应用层定义的简单值对象 T convert(String userId, String username, LocalDateTime createdAt); } - 应用服务的方法接收命令和
ResultConverter<T>实例,处理完领域逻辑后,提取领域对象的字段调用转换器:@Service public class UserApplicationService { private final UserRepository userRepository; public <T> T handleCreateUserCommand(CreateUserCommand command, ResultConverter<T> converter) { // 执行领域逻辑:创建并保存用户领域对象 User user = User.create(command.getUsername(), command.getEmail()); userRepository.save(user); // 仅传递字段值,绝不暴露User领域对象给外部 return converter.convert(user.getId().value(), user.getUsername(), user.getCreatedAt()); } } - 在
rest-adapter模块(依赖application模块)实现ResultConverter<T>,返回REST需要的DTO:@Component public class UserDtoConverter implements ResultConverter<UserDto> { @Override public UserDto convert(String userId, String username, LocalDateTime createdAt) { UserDto dto = new UserDto(); dto.setId(userId); dto.setUsername(username); dto.setCreatedAt(createdAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); return dto; } } - REST控制器调用应用服务时传入转换器:
@RestController @RequestMapping("/users") public class UserController { private final UserApplicationService userService; private final UserDtoConverter dtoConverter; @PostMapping public ResponseEntity<UserDto> createUser(@RequestBody CreateUserRequest request) { CreateUserCommand command = new CreateUserCommand(request.getUsername(), request.getEmail()); UserDto userDto = userService.handleCreateUserCommand(command, dtoConverter); return ResponseEntity.status(HttpStatus.CREATED).body(userDto); } }
这个方案严格遵循六边形架构的依赖规则(内层不依赖外层),同时完美实现了领域模型的封装,客户端可以自由定义输出类型。
方案2:泛型回调+领域数据提取器(更灵活的轻量方案)
如果不想为每个领域对象定义单独的端口接口,可以用Function回调结合应用层的领域数据提取器,核心还是让应用层负责拆解领域对象,避免客户端接触领域模型。
具体步骤:
- 在
application模块定义一个DomainData类,专门用于封装从领域对象提取的字段:public class DomainData { private final Map<String, Object> fields; private DomainData(Map<String, Object> fields) { this.fields = fields; } // 针对不同领域对象的提取方法 public static DomainData fromUser(User user) { return new DomainData(Map.of( "userId", user.getId().value(), "username", user.getUsername(), "createdAt", user.getCreatedAt() )); } // 类型安全的字段获取方法 @SuppressWarnings("unchecked") public <V> V get(String fieldName) { return (V) fields.get(fieldName); } } - 应用服务方法接收命令和
Function<DomainData, T>转换器:public <T> T handleCreateUserCommand(CreateUserCommand command, Function<DomainData, T> converter) { User user = User.create(command.getUsername(), command.getEmail()); userRepository.save(user); // 转换为DomainData后传递给客户端转换器 return converter.apply(DomainData.fromUser(user)); } - REST控制器中直接传入lambda完成转换:
@PostMapping public ResponseEntity<UserDto> createUser(@RequestBody CreateUserRequest request) { CreateUserCommand command = new CreateUserCommand(request.getUsername(), request.getEmail()); UserDto userDto = userService.handleCreateUserCommand(command, data -> { UserDto dto = new UserDto(); dto.setId(data.get("userId")); dto.setUsername(data.get("username")); dto.setCreatedAt(data.get("createdAt").format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); return dto; }); return ResponseEntity.status(HttpStatus.CREATED).body(userDto); }
这个方案更灵活,不需要额外定义接口,适合快速迭代场景,同时保持了领域模型的封装性。
方案3:应用层返回抽象结果,适配器层映射(统一操作状态处理)
如果你的系统需要统一处理操作成功/失败的状态,可以让应用层返回一个抽象的OperationResult对象,再由适配器层映射为具体的DTO。
具体步骤:
- 在
application模块定义OperationResult:public class OperationResult { private final boolean success; private final Map<String, Object> data; private final String errorMessage; // 私有构造方法,通过静态工厂方法创建 private OperationResult(boolean success, Map<String, Object> data, String errorMessage) { this.success = success; this.data = data; this.errorMessage = errorMessage; } public static OperationResult success(Map<String, Object> data) { return new OperationResult(true, data, null); } public static OperationResult failure(String errorMessage) { return new OperationResult(false, null, errorMessage); } // getter方法省略 } - 应用服务返回
OperationResult:public OperationResult handleCreateUserCommand(CreateUserCommand command) { try { User user = User.create(command.getUsername(), command.getEmail()); userRepository.save(user); return OperationResult.success(Map.of( "userId", user.getId().value(), "username", user.getUsername(), "createdAt", user.getCreatedAt() )); } catch (DomainValidationException e) { return OperationResult.failure(e.getMessage()); } } - 在
rest-adapter模块定义ResultMapper<T>接口并实现:public interface ResultMapper<T> { T map(OperationResult result); } @Component public class UserResultMapper implements ResultMapper<UserDto> { @Override public UserDto map(OperationResult result) { if (!result.isSuccess()) { throw new BadRequestException(result.getErrorMessage()); } UserDto dto = new UserDto(); dto.setId((String) result.getData().get("userId")); dto.setUsername((String) result.getData().get("username")); dto.setCreatedAt(((LocalDateTime) result.getData().get("createdAt")).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); return dto; } } - 控制器调用:
@PostMapping public ResponseEntity<UserDto> createUser(@RequestBody CreateUserRequest request) { CreateUserCommand command = new CreateUserCommand(request.getUsername(), request.getEmail()); OperationResult result = userService.handleCreateUserCommand(command); UserDto userDto = userResultMapper.map(result); return ResponseEntity.status(HttpStatus.CREATED).body(userDto); }
这个方案的好处是应用层不需要依赖客户端的任何代码,结果更通用,适合需要统一处理操作状态的系统。
关键注意事项
- 模块依赖管控:确保
rest-adapter仅依赖application模块,domain模块不依赖任何外层模块,严格遵循六边形架构的依赖方向(内层定义端口,外层实现端口)。 - 类型安全保障:如果使用
Map传递数据,建议用强类型的DomainData或值对象替代,避免运行时类型转换错误。 - 领域模型封装:绝对不要将领域对象的引用传递给外层,只传递提取后的字段值,保持领域模型的完整性和封装性。
内容的提问来源于stack exchange,提问作者Archange
相关产品推荐
相关产品推荐

