Search code examples
javaoopinheritancebuilderdiscord

How to follow best Java practices using builder pattern for a subclass of an abstract superclass?


// CommandBuilder.java
public class CommandBuilder {

 public String name; // required
 public String description = "Default Value"; //optional

 public CommandBuilder(String name) {
  this.name = name;
 }

 public CommandBuilder setDescription(String description) {
  this.description = description;
  return this;
 }

 public CommandBuilder build() {
  return this;
 }
}

// Command.java
public abstract class Command extends ListenerAdapter {

 private String name;
 private String description;

 protected abstract void execCommand(MessageReceivedEvent event);

 public Command(CommandBuilder builder) {
  this.name = builder.name;
  this.description = builder.description;
 }

@Override
public void onMessageReceived(MessageReceivedEvent event) {
   execCommand(event);
 }
}

// ExampleCommand.java
public class ExampleCommand extends Command {

 public ExampleCommand(CommandBuilder builder) {
  super(builder);
 }

 @Override
 protected void execCommand(MessageReceivedEvent event) {
  // ...
 }
}

// Bot.java
public class Bot() {

 public static void main(String[] args) {

  // ...
  jdaBuilder.addEventListener(
   new ExampleCommand(
    new CommandBuilder("Example Command").setDescription("You know it.").build();
   )
  );
  // ...
 }
}

So, I need some advice on code style. This above is roughly my code setup for a Discord bot in JDA. What jdaBuilder.addEventListener(Object) does or what MessageReceivedEvent is, is not important.

I use the builder pattern to avoid excessive constructor overloading when constructing objects with the inherited type Command, because in my actual code the Command class can accept far more than just two parameters. The issue with CommandBuilder is that build() doesn't and can't return an object of type Command (because it's abstract) but rather the type CommandBuilder itself, that each subclass of Command takes as an argument (and then passes on to Command). The problem with THAT in return is that:

  1. Calling build() is not required because every single other method returns a CommandBuilderaswell
  2. Instantiation of a subclass of Command with 4-6 parameters can get very messy in the main.

So, what's the best way to solve this? I thought of using an interface, but in my abstract Command class there are certain methods with "default" code, that the subclasses can choose to override if they need it (but it's not required!). These default methods utilize the other methods of the Command class, so I can't just refactor them into the interface as true default methods.

My code works just fine, I just think that with the way I have to instantiate my objects, I did a wrong turn somewhere. Any advice on how to refactor or rewrite my code to follow best Java practices?


Solution

  • Maybe you shoud refactor your build() like this:

    public <T extends Command> T build(Function<CommandBuilder, T> constructor) {
        return constructor.apply(this);
    }
    

    and this way allow the builder to accept any constructor reference such as

    FooCommand cmd = builder.build(FooCommand::new);
    

    Maybe additionally you could add proper code to alternatively allow for reflection so that

    FooCommand cmd = builder.build(FooCommand.class);
    

    would usable as well, but the former one has the advantage that it is a compile time error if you don't have a matching constructor.