简体   繁体   中英

Generics and selecting correct interface implementation at runtime

I'm working on a PoC of an ES & CQRS system.

I have defined following classes to represent commands and events that represent an output of command being handled

public class CreateEstateCommand extends Command {}

public class ChangeEstateOwnerCommand extends Command {}

public class EstateCreatedEvent extends DomainEvent {}

public class EstateOwnerChangedEvent extends DomainEvent {}

The commands are being handled in classes that implement the following interface

/**
 * 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) { /***/ }
}

Now there's the part where I want to use those specific handlers. The flow of command handling can be represented as follows:

Command gets into the system through the API, and they are forwarded to CommandServce class for handling

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);
    }
}

As you can see, the handleCommand() methods are duplicated for each of the command submitted. The reason behind this is the problem I have with selecting appropriate handler implementation at runtime, depending on 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);
    }

}

The snippet above is the part I cannot generify. Is it possible, to implement CommandGateway class, so CommandService can look as follows:

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

And provide type-safe objects?

The root problem is the map, whose values are wildcard typed, ie effectively untyped, and more particularly, not typed to align with the key.

You've already ripped up some typed safety by trusting the injected map's entries, so just take it one step further by using a raw CommandHandler , which will accept any command, and use an unchecked cast for a properly typed return value:

@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 added so neither your IDE nor build complain.

While this might seem brutal, you haven't actually lost any type safety. That was lost when you typed your map as you did, which unfortunately was unavoidable given that map typing does not bind the value type to the key type.

What if you do something like this:

static abstract class Command {

    public abstract String getCommandType();

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

And your implementation:

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

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

And usage would be:

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

The difficulty in achieving this end-to-end with generics is that we are mixing runtime type decisions with compile-time type checks . You design is close to an MVC design, where handlers are retrieved at runtime. So, only limited type-safety checks are possible at compile-time.

Let us look at a flow:

  • An API recieves a command with its inputs
  • You may have a specific controller method that knows the command and its inputs and can construct one of CreateEstateCommand or ChangeEstateOwnerCommand
  • Now, at runtime we have to access the map to know the associated CommandHandler . Now, the very reason we have to do this at runtime is because the decision cannot be made at compile time, due to the design of using a Map .

If you have the following method in CommandGateway , it can never be sure if the Command instance passed from the service class is really the specific child that the mapped CommandHandler wants. Hence, it will not compile.

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

Hence, you have to do away with either the end-to-end type safety requirement or with the "factory" style map that gives you the CommandHandler instance. Leaving out the latter will mean that the API method, right in the beginning, knows which CommandHandler it needs.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM