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
CommandRouter
s. 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 CommandRouter
s with different Map
s,
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
@Module
s. 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 Command
s 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 theUserCommandsModule
that declares ourDepositCommand
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 theAccount
instance we pass as an argument should be requestable by any@Inject
constructor,@Binds
method, or@Provides
method 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.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.
- There is a parallel annotation,
@Component.Factory
, for@Component
.@BindsInstance
parameters let you make arbitrary objects requestable by other binding methods in the component.