Home > Mobile >  Generics and selecting correct interface implementation at runtime
Generics and selecting correct interface implementation at runtime

Time:11-28

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?

CodePudding user response:

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.

CodePudding user response:

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()));
}
  • Related