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.
Since we’re changing from only creating a single CommandRouter at the root to
creating a CommandProcessor, we’ll rename the component to match.
We can rename our root @Component from CommandRouterFactory to
CommandProcessorFactory, and have it provide our new CommandProcessor.
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
@Subcomponentannotation 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 theUserCommandsModulethat declares ourDepositCommandhere. - The
router()method declares what object we want Dagger to create. - The
@Subcomponent.Factoryannotation 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
@BindsInstanceparameter, which tells Dagger that theAccountinstance we pass as an argument should be requestable by any@Injectconstructor,@Bindsmethod, or@Providesmethod in this subcomponent.
- It has a single method that creates an instance of the subcomponent.
That method has a
- We have a module that declares the subcomponent. Including this module in
another component will make the
@Subcomponent.Factoryavailable there. That’s our bridge between the two components.
Now that we’ve moved UserCommandsModule to the UserCommandsRouter
subcomponent, we need to remove it from the root CommandProcessorFactory and
install UserCommandsRouter.InstallationModule instead.
We do this because those commands should only be available after login. If they
were in the root component, they’d be available immediately. This also shows up
as a technical dependency: DepositCommand needs an Account, which is only
available in the subcomponent (which is itself created after login).
@Singleton
@Component(
modules = {
LoginCommandModule.class,
HelloWorldModule.class,
UserCommandsRouter.InstallationModule.class,
SystemOutModule.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.
- There is a parallel annotation,
@Component.Factory, for@Component.@BindsInstanceparameters let you make arbitrary objects requestable by other binding methods in the component.