【问题标题】:Generics and selecting correct interface implementation at runtime泛型和在运行时选择正确的接口实现
【发布时间】:2021-11-27 16:41:00
【问题描述】:

我正在研究 ES 和 CQRS 系统的 PoC。

我定义了以下类来表示命令和事件,这些命令和事件代表正在处理的命令的输出

public class CreateEstateCommand extends Command {}

public class ChangeEstateOwnerCommand extends Command {}

public class EstateCreatedEvent extends DomainEvent {}

public class EstateOwnerChangedEvent extends DomainEvent {}

在实现以下接口的类中处理命令

/**
 * Specific command handlers define what logic should be carried out during handling a command of type C.
 * Single command execution results in an outcome of domain event of type E
 */
public interface CommandHandler<C extends Command, E extends DomainEvent> {
  E handleCommand(C command);
}

public class EstateCreatedCommandHandler implements CommandHandler<CreateEstateCommand, EstateCreatedEvent> {
    @Override
    public EstateCreatedEvent handleCommand(CreateEstateCommand command) { /***/ }
}

public class ChangeEstateOwnerCommandHandler implements CommandHandler<ChangeEstateOwnerCommand, EstateOwnerChangedEvent> {
    @Override
    public EstateOwnerChangedEvent handleCommand(ChangeEstateOwnerCommand command) { /***/ }
}

现在是我想使用这些特定处理程序的部分。命令处理的流程可以表示如下:

命令通过API进入系统,转发给CommandServce类处理

public class CommandService {

    private final EventService eventService;
    private final CommandGateway commandGateway;

    public void handleCommand(CreateEstateCommand command) {
        EstateCreatedEvent event = commandGateway.handleCommand(command);
        eventService.handleEvent(event);
    }

    public void handleCommand(ChangeEstateOwnerCommand command) {
        EstateOwnerChangedEvent event = commandGateway.handleCommand(command);
        eventService.handleEvent(event);
    }
}

如您所见,handleCommand() 方法对于每个提交的命令都是重复的。这背后的原因是我在运行时选择适当的处理程序实现时遇到的问题,具体取决于Command.commandType

@Service
public class CommandGateway {

    private final Map<String, CommandHandler<?, ?>> commandHandlers;

    @Autowired
    public CommandGateway(Map<String, CommandHandler<?, ?>> commandHandlers) {
        this.commandHandlers = commandHandlers;
    }

    public EstateCreatedEvent handleCommand(CreateEstateCommand command) {
        EstateCreatedCommandHandler handler = (EstateCreatedCommandHandler) commandHandlers.get(command.getCommandType());
        return handler.handleCommand(command);
    }


    public EstateOwnerChangedEvent handleCommand(ChangeEstateOwnerCommand command) {
        ChangeEstateOwnerCommandHandler handler = (ChangeEstateOwnerCommandHandler) commandHandlers.get(command.getCommandType());
        return handler.handleCommand(command);
    }

}

上面的sn-p是我无法概括的部分。是否有可能,实现CommandGateway 类,所以CommandService 可以如下所示:

public class CommandService {
    
    public <C extends Command, E extends DomainEvent> void handleCommand(C command) {
        E event = commandGateway.handleCommand(command);
    }
}

并提供类型安全的对象?

【问题讨论】:

  • 我很确定“提供类型安全的对象”部分不是。泛型是仅编译时的构造,它们在运行时消失。而且由于您的查找是在运行时进行的,因此无法涉及泛型。为此,您将需要一些运行时信息。寻找 Joshua Block 的“类型安全异构容器”模式的想法。

标签: java generics


【解决方案1】:

根本问题是映射,它的值是通配符类型的,即实际上是无类型的,更具体地说,没有键入以与键对齐。

您已经通过信任注入映射的条目而破坏了一些类型的安全性,因此只需使用原始的CommandHandler 更进一步,它将接受任何命令,并使用未经检查的强制转换来正确键入返回价值:

@SuppressWarnings({"unchecked", "rawtypes"})
public <C extends Command, E extends DomainEvent> E handleCommand(C command) {
    CommandHandler handler =  commandHandlers.get(command.getCommandType());
    return (E)handler.handleCommand(command);
}

@SuppressWarnings 已添加,因此您的 IDE 和构建都不会抱怨。

虽然这看起来很残酷,但您实际上并没有失去任何类型安全性。当您像以前一样键入地图时,这会丢失,不幸的是,鉴于地图键入不会将值类型绑定到键类型,这是不可避免的。

【讨论】:

  • “您已经通过信任注入映射的条目来破坏一些类型的安全性” - 此处使用映射只是因为此类 bean 的方便弹簧自动装配 - 使用它没有特别的论据。您是否还有其他不涉及此类地图并且更适合这种情况的想法?
  • @bwylegly 我自己也遇到过几次类似的情况。不幸的是,没有解决方案可以避免 somewhere 的强制转换。以Map&lt;Class&lt;?&gt;, Object&gt;&gt; 的简单示例为例,我知道 值对象是类键的一个实例,但是无法键入映射,因此我可以编写MyClass obj = map.get(MyClass.class);。您可以使用实现此目的的映射字段创建包装类,但这只是将未经检查的强制转换移动到包装类中。未经检查的演员必须去某个地方 - 你应该选择你最舒服的地方。
【解决方案2】:

如果你这样做会怎样:

static abstract class Command {

    public abstract String getCommandType();

    public abstract Class<? extends DomainEvent> type();
}

还有你的实现:

public class CreateEstateCommand extends Command {
    @Override
    public String getCommandType() {
        return null; // whatever here
    }

    @Override
    public Class<EstateCreatedEvent> type() {
        return EstateCreatedEvent.class;
    }
}

用法如下:

public DomainEvent handleCommand(Command command) {
    return command.type().cast(commandHandlers.get(command.getCommandType()));
}

【讨论】:

    【解决方案3】:

    使用泛型实现这种端到端的困难在于我们将运行时类型决策编译时类型检查混合在一起。您的设计接近于 MVC 设计,其中处理程序是在运行时检索的。因此,在编译时只能进行有限的类型安全检查。

    让我们看一个流程:

    • API 接收带有其输入的命令
    • 您可能有一个特定的控制器方法,它知道命令及其输入,并且可以构造CreateEstateCommandChangeEstateOwnerCommand 之一
    • 现在,在运行时我们必须访问地图以了解关联的CommandHandler。现在,我们必须在运行时执行此操作的真正原因是,由于使用 Map 的设计,无法在编译时做出决定。

    如果您在CommandGateway 中有以下方法,则永远无法确定从服务类传递的Command 实例是否真的是映射的CommandHandler 想要的特定子。因此,它不会编译。

    public DomainEvent handleCommand( Command command ) {
        CommandHandler<? extends Command, ? extends DomainEvent> cmdHandler = commandHandlers.get(command.getCommandType());
        return cmdHandler.handleCommand( command );
    }
    

    因此,您必须取消端到端类型安全要求或为您提供CommandHandler 实例的“工厂”样式映射。省略后者意味着 API 方法一开始就知道它需要哪个CommandHandler

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多