You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Gradle多模块项目六边形架构:应用层输出转换方案咨询

这是个非常典型的六边形架构下的封装与适配问题,我刚好有几个成熟的方案可以帮你解决,完全符合你不暴露领域模型、让客户端控制输出的需求:

方案1:基于输出端口的泛型转换(贴合六边形架构设计)

在六边形架构中,应用层的核心职责之一是定义输出端口(Output Port),外部适配器(比如你的rest-adapter)通过实现这些端口来完成结果的转换。这里的关键是让输出端口成为泛型接口,且完全不依赖领域模型——应用层会主动提取领域对象的必要字段,传递给端口实现。

具体步骤:

  1. 在application模块(应用层)定义泛型输出端口接口:
    public interface ResultConverter<T> {
        // 这里只暴露领域模型需要对外提供的字段,用原始类型或应用层定义的简单值对象
        T convert(String userId, String username, LocalDateTime createdAt);
    }
    
  2. 应用服务的方法接收命令和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());
        }
    }
    
  3. 在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;
        }
    }
    
  4. 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回调结合应用层的领域数据提取器,核心还是让应用层负责拆解领域对象,避免客户端接触领域模型。

具体步骤:

  1. 在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);
        }
    }
    
  2. 应用服务方法接收命令和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));
    }
    
  3. 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。

具体步骤:

  1. 在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方法省略
    }
    
  2. 应用服务返回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());
        }
    }
    
  3. 在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;
        }
    }
    
  4. 控制器调用:
    @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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:40:24