Only allow depositing after logging in

The current UX design of this ATM can be improved by only supporting deposit commands after one has already logged in. To do that, let’s first perform a refactoring.

Refactoring: Introducing a CommandProcessor

We can introduce a CommandProcessor that contains a stack of CommandRouters. Pushing onto the stack will enable a new set of commands to be processed. Popping will return to the previous set of commands.

@Singleton
final class CommandProcessor {
  private final Deque<CommandRouter> commandRouterStack = new ArrayDeque<>();

  @Inject
  CommandProcessor(CommandRouter firstCommandRouter) {
    commandRouterStack.push(firstCommandRouter);
  }

  Status process(String input) {
    Result result = commandRouterStack.peek().route(input);
    if (result.status().equals(Status.INPUT_COMPLETED)) {
      commandRouterStack.pop();
      return commandRouterStack.isEmpty()
          ? Status.INPUT_COMPLETED : Status.HANDLED;
    }

    result.nestedCommandRouter().ifPresent(commandRouterStack::push);
    return result.status();
  }
}

We can modify Command as follows:

interface Command {
  Result handleInput(List<String> input);

  final class Result {
    private final Status status;
    private final Optional<CommandRouter> nestedCommandRouter;

    private Result(Status status, Optional<CommandRouter> nestedCommandRouter) {
      this.status = status;
      this.nestedCommandRouter = nestedCommandRouter;
    }

    static Result invalid() {
      return new Result(Status.INVALID, Optional.empty());
    }

    static Result handled() {
      return new Result(Status.HANDLED, Optional.empty());
    }

    Status status() {
      return status;
    }

    Optional<CommandRouter> nestedCommandRouter() {
      return nestedCommandRouter;
    }

    static Result enterNestedCommandSet(CommandRouter nestedCommandRouter) {
      return new Result(Status.HANDLED, Optional.of(nestedCommandRouter));
    }
  }

  enum Status {
    INVALID,
    HANDLED,
    INPUT_COMPLETED
  }
}

CommandProcessor is marked with @Singleton to ensure that only one CommandRouter stack is created.

We can rename our @Component to CommandProcessorFactory. Now it looks like this:

@Singleton
@Component(
    modules = {
      LoginCommandModule.class,
      HelloWorldModule.class,
      UserCommandsModule.class,
      SystemOutModule.class
    })
interface CommandProcessorFactory {
  CommandProcessor commandProcessor();
}

Finally, we can refactor CommandLineAtm accordingly:

class CommandLineAtm {
  public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    CommandProcessorFactory commandProcessorFactory =
        DaggerCommandProcessorFactory.create();
    CommandProcessor commandProcessor =
        commandProcessorFactory.commandProcessor();

    while (scanner.hasNextLine()) {
      Status unused = commandProcessor.process(scanner.nextLine());
    }
  }
}

Creating nested commands

Dagger already knows how to create a CommandRouter from a Map<String, Command>, but those commands apply to all users. That requires specifying the username for every command. So that we don’t have to do that, let’s add a concept of a user session and commands that only apply while a user is logged in. But how can we create two different CommandRouters with different Maps, one for logged out users and one for logged in users?

We can introduce a @Subcomponent to help. A @Subcomponent is similar to a @Component: it has abstract methods that Dagger implements, and it can use @Modules. It always has a parent component, and it can access any type that the parent component can access. Any types it creates are hidden from the parent component.

Let’s create a @Subcomponent that adds Commands for a logged-in user. It will share the same Database that exists in the CommandProcessorFactory component, which will be useful when we want to deposit and withdraw money from a particular user.

@Subcomponent(modules = UserCommandsModule.class)
interface UserCommandsRouter {
  CommandRouter router();

  @Subcomponent.Factory
  interface Factory {
    UserCommandsRouter create(@BindsInstance Account account);
  }

  @Module(subcomponents = UserCommandsRouter.class)
  interface InstallationModule {}
}

There are a few things that are happening here. Let’s break it down:

  • The @Subcomponent annotation defines what modules Dagger should know about when creating instances for this subcomponent only. Just like @Component, it can take a list of modules: we’ve moved the UserCommandsModule that declares our DepositCommand here.
  • The router() method declares what object we want Dagger to create.
  • The @Subcomponent.Factory annotation annotates a factory type for this subcomponent. It’s an interface we define.
    • It has a single method that creates an instance of the subcomponent. That method has a @BindsInstance parameter, which tells Dagger that the Account instance we pass as an argument should be requestable by any @Inject constructor, @Binds method, or @Provides method in this subcomponent.
  • We have a module that declares the subcomponent. Including this module in another component will make the @Subcomponent.Factory available there. That’s our bridge between the two components.

We need to include UserCommandsRouter.InstallationModule in our @Component annotation:

@Singleton
@Component(
    modules = {
      ...
      UserCommandsRouter.InstallationModule.class,
    })
interface CommandProcessorFactory {
  CommandProcessor commandProcessor();
}

Now we can start to use UserCommandsRouter in LoginCommand:

final class LoginCommand extends SingleArgCommand {
  private final Database database;
  private final Outputter outputter;
  private final UserCommandsRouter.Factory userCommandsRouterFactory;

  @Inject
  LoginCommand(
      Database database,
      Outputter outputter,
      UserCommandsRouter.Factory userCommandsRouterFactory) {
    this.database = database;
    this.outputter = outputter;
    this.userCommandsRouterFactory = userCommandsRouterFactory;
  }

  @Override
  public Result handleArg(String username) {
    Account account = database.getAccount(username);
    outputter.output(
        username + " is logged in with balance: " + account.balance());
    return Result.enterNestedCommandSet(
        userCommandsRouterFactory.create(account).router());
  }
}

Now deposit is only valid after logging in! We can also remove the username from the command syntax because it is being provided already with the @BindsInstance parameter:

final class DepositCommand extends BigDecimalCommand {
  private final Account account;
  private final Outputter outputter;

  @Inject
  DepositCommand(Account account, Outputter outputter) {
    super(outputter);
    this.account = account;
    this.outputter = outputter;
  }

  @Override
  public void handleAmount(BigDecimal amount) {
    account.deposit(amount);
    outputter.output(account.username() + " now has: " + account.balance());
  }
}

(The abstract BigDecimalCommand we’re using for simplicity is defined here).

CONCEPTS

  • A @Subcomponent-annotated type (a “subcomponent”) is, like a @Component-annotated one, a factory for an object. Like @Component, it uses modules to give Dagger implementation instructions. Subcomponents always have a parent component (or a parent subcomponent), and any objects that are requestable in the parent are requestable in the child, but not vice versa.
  • A @Subcomponent.Factory-annotated type creates instances of the subcomponent. An instance of it is requestable in the parent component.
  • @BindsInstance parameters let you make arbitrary objects requestable by other binding methods in the component.

Previous · Next