Object-Oriented Solutions: Accounts and Currencies
To avoid maintenance problems and implement new features, you might start with Object-Oriented Design and utilize localized changes.
Join the DZone community and get the full member experience.
Join For FreeThis article is about a simplified version of real code running at a financial institution and how maintenance problems with this code can be avoided by using Object-Oriented Design. The purpose of the code is to represent retail money Accounts and enable transferring money, define recurring transfers and to support the usual functionality you find at any bank.
First, there is an Account interface and a Money class:
public interface Account {
String getAccountNumber();
Currency getCurrency(); // The base currency of the account
...
}
public class Money {
...
public BigDecimal getValue() {
...
}
public Currency getCurrency() {
...
}
}
All Accounts have a given Currency in which the money is held in the Account. All operations on an Account must use the same Currency with which the Account is defined, so it is not possible to transfer Money to or from the Account if the Currencies do not match. Currency is an enumeration:
public enum Currency {
EUR, USD, ...;
}
For all these components, there are Services to execute a given business case, like Cash Transfer for example.
public class CashTransferService {
...
public void execute() {
if (account.getCurrency() != money.getCurrency()) {
throw new CashException("tried to transfer in non-matching currency");
}
...
}
...
}
The Service above contains the logic to transfer Money between Accounts and the first step is to check whether the Currencies match.
This kind of design is surprisingly common in the Java Enterprise field, and a lot of developers would perhaps not see any problem with this approach at all. Unfortunately, this design leads directly to code quality problems, unreadable and unmaintainable code. Let’s see what the next feature request caused with this code:
International Accounts
After the code above was already in production the bank decided that customers should have the possibility of choosing a new type of Account which would accept any Currency and would convert between mismatching Currencies “on the fly”. Let’s see how this feature was implemented to the above-shown parts of the Account:
The new Account type had no default currency, so it’s implementation simply returned null:
public class InternationalAccount implements Account {
...
public Currency getCurrency() {
return null;
}
...
}
Of course, the condition in the CashTransferService also needs to be modified, because it now has to handle null values for Currency:
if (account.getCurrency() != null && account.getCurrency() != money.getCurrency() {
...
}
The developer responsible for this modification then even took the time to make the code more readable by extracting the condition into a method in the Service:
if (!hasMatchingCurrency(account, money)) {
...
}
...
public boolean hasMatchingCurrency(Account account, Money money) {
return account.getCurrency() == null || account.getCurrency() == money.getCurrency();
}
Although this code is generally not regarded as “bad,” the death spiral of code quality has already begun and it will not stop until structural changes are made. Specifically, the following pitfalls can be already clearly seen:
- The modification required the knowledge of other code. We knew, that the condition in CashTransferService had to be modified, but in fact it is very easy to miss other places where a null value as Currency might cause problems. In short, the change is not localized.
- Null value has special semantics. The developer decided that null as AccountCurrency means that all Currencies are accepted. This is completely arbitrary and non-obvious, thus the API of Account became more obfuscated. Note that Optional does not help with this at all. Neither does documentation.
- Duplicated code. The same condition is in fact found in many places, since there are multiple Services working with Accounts. Each of those now has to in effect duplicate the Currency checking logic. Moving the “hasMatchingCurrency()” method to a utility is only a band-aid, but does not solve the problem.
- Business functionality diffuses, becomes less clear. Since checks need to be done externally to the Account, it is inevitable that some logic will diffuse and probably will lead to multiple versions of the same check. It also contributes to changes not being local, and eroding responsibilities.
Fixing the Code With OO, First Step
These kinds of problems almost always have the same cause: misplaced responsibilities. The original Account interface, through publishing the base Currency of the account, places the responsibility to check for Currency conformity at the caller site. The callers (which are all related Services) need to know how the Account works, and need to be able to check for preconditions. This indirectly leads to all the problems listed above.
To fix this, the responsibility of checking for this condition needs to be put into the Account itself. For example this way:
public interface Account {
...
boolean allowsTransactionCurrency(Currency currency);
...
}
This is a small, very important, and sometimes confusing step. The basic idea is, that we are not asking the object for data (like getCurrency() does), instead we trust the Account to make a decision itself.
With this interface, defining the International Account is easy:
public class InternationalAccount implements Account {
...
public boolean allowsTransactionCurrency(Currency currency) {
return true;
}
...
}
Please also note the following consequences:
- There is no need to adjust any of the Services (if they already use the new interface), introducing the InternationalAccount is completely localized.
- There is no need to introduce null, or any other special values or meaning.
- There is still some duplicated code, since all Services still need to know to call the allowsTransactionCurrency() method, but it is less code than before.
Daily Account
The next feature request was, that there should be an Account type, which can be easily used daily, with low fees, quicker transactions, but all the transactions should be limited to some amount, let’s say 200 EUR.
As before, the first solution looked like this:
public interface Account {
...
BigDecimal getLimit();
...
}
It introduced an Account limit value, which of course then needs to be checked in every Service that transfers money from the Account (of which there are many).
Again, this might seem like a perfectly reasonable solution for many developers, but unfortunately leads to exactly the same problems as before. There will be Accounts with no inherent limits, then those need special values, and all this has to be checked by all Services the same way. Introducing duplication, diffusing responsibility, and starting bit rot.
The same solution as above can be applied here this way:
public interface Account {
...
boolean supportsWithdrawAmount(BigDecimal amount);
...
}
Although this is a much better approach, let’s look at the CashTransferService again:
public class CashTransferService {
...
public void execute() {
if (!account.supportsWithdrawAmount(amount) || !account.allowsTransactionCurrency(currency)) {
throw new CashException("...");
}
...
}
...
}
All these features start to accumulate in the Services, probably with copy-paste code to check for the different conditions an Account might have, depending on what transaction the Service wants to initiate.
Fixing the Code With OO, Second Step
So what is wrong with the code above? Why are these problems still popping up? The answer to that might seem familiar: misplaced responsibilities.
The problem is still that we don’t trust the Account object enough, that is why we have to know how to micromanage it. We ask it to make some decisions, but we don’t trust it with the big picture. The big picture being: transferring money to an external account.
Instead of externalizing logic on how to transfer Money out of an Account, this is something the Account is actually perfectly capable of doing, if we just let it:
public interface Account {
...
void transferTo(ExternalAccountReference targetReference, Money money);
...
}
This approach does not only get rid of all the superfluous “checks” that were previously in the Account class, but it gets rid of the Services related to Accounts.
Not only that, but it is actually a truer representation of the business. It is easier to understand what an Account can be used for, and it is easier to understand the relations between different business objects.
Summary
This article demonstrates through a simplified example what it means to think more in an Object-Oriented way instead of the usual Data- and Service-oriented approach.
It also shows how to use Object-Oriented Design to avoid maintenance problems and make implementing new features easy by localizing changes, placing code that needs to be changed together in the same class, and thinking about trusting objects with responsibilities instead of micromanaging them.
Published at DZone with permission of Robert Brautigam. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments