Friday, December 6, 2019

Banking: Skip to the Owl

I've been looking at the Codurance Banking exercise this evening:

Given a client makes a deposit of 1000 on 10-01-2012
And a deposit of 2000 on 13-01-2012
And a withdrawal of 500 on 14-01-2012
When they print their bank statement
Then they would see:

Date       || Amount || Balance
14/01/2012 || -500   || 2500
13/01/2012 || 2000   || 3000
10/01/2012 || 1000   || 1000

which is introduced with an additional interface constraint
public interface AccountService
{
    void deposit(int amount); 
    void withdraw(int amount); 
    void printStatement();
}

Here are the things that I see when I look at this problem.

First, I notice that we have dates. That implies that somewhere I'm going to need a calendar or a clock, because my interface doesn't have way to provide a date. Similarly, I'm going to need some sort of output device that accepts the statement. I'm going to need some way to capture the dates and amounts so that they are available to be printed - a storage data structure of some form. And I'm going to need some functional compute to actually do the work.

Let's draw the rest of the owl!

class TheRestOfTheOwl implements AccountService {
    interface StatelessCore {
        void deposit(int amount, Clock clock, Storage storage);
        void withdraw(int amount, Clock clock, Storage storage);
        void printStatement(Storage storage, Console console);
    }
    
    private final Clock clock;
    private final Storage storage;
    private final Console console;
    private final StatelessCore core;

    TheRestOfTheOwl(Clock clock, Storage storage, Console console, StatelessCore core) {
        this.clock = clock;
        this.storage = storage;
        this.console = console;
        this.core = core;
    }

    @Override
    public void deposit(int amount) {
        core.deposit(amount, clock, storage);
    }

    @Override
    public void withdraw(int amount) {
        core.withdraw(amount, clock, storage);
    }

    @Override
    public void printStatement() {
        core.printStatement(storage, console);
    }
}

You could write tests for that, but I don't see how you are going to get a lot of return out of that investment.

Now, let's suppose you didn't have the insight that Storage is a separate concept from the working core; you might have guessed that you could keep everything in an in-memory data structure, for example, because in your independent tests the lifetime of "the" account is coincident with the test universe itself. So you might start with a different owl:

class SimpleOwl implements AccountService {
    interface StatelessCore {
        void deposit(int amount, Clock clock);
        void withdraw(int amount, Clock clock);
        void printStatement(Console console);
    }

    private final Clock clock;
    private final Console console;
    private final StatelessCore core;

    SimpleOwl(Clock clock, Console console, StatelessCore core) {
        this.clock = clock;
        this.console = console;
        this.core = core;
    }
    
    @Override
    public void deposit(int amount) {
        core.deposit(amount, clock);
    }

    @Override
    public void withdraw(int amount) {
        core.withdraw(amount, clock);
    }

    @Override
    public void printStatement() {
        core.printStatement(console);
    }
}

Riddle: what happens now when you discover that storage should be a separate component?

One possible answer is to recognize that SimpleOwl is simple enough to throw away when it isn't useful any more.

Another possibility is to introduce an adapter....

class Adapter implements SimpleOwl.StatelessCore {
    final Storage storage;
    final TheRestOfTheOwl.StatelessCore core;

    Adapter(Storage storage, TheRestOfTheOwl.StatelessCore core) {
        this.storage = storage;
        this.core = core;
    }

    @Override
    public void deposit(int amount, Clock clock) {
        core.deposit(amount, clock, storage);
    }

    @Override
    public void withdraw(int amount, Clock clock) {
        core.withdraw(amount, clock, storage);
    }

    @Override
    public void printStatement(Console console) {
        core.printStatement(storage, console);
    }
}

Note that this illustrates that "Stateless Core" is a really lousy interface name; Adapter is a core that has state. Naming is one of the two hard problems.

You'd see a similar scramble when you realize that the description of your use case doesn't have any sort of representation of a unique account identifier to ensure that you retrieve the correct information from shared storage.

No comments:

Post a Comment