DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report

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.

Robert Brautigam user avatar by
Robert Brautigam
·
Mar. 23, 17 · Tutorial
Like (15)
Save
Tweet
Share
12.67K Views

Join the DZone community and get the full member experience.

Join For Free

This 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.

Popular on DZone

  • 5 Steps for Getting Started in Deep Learning
  • Journey to Event Driven, Part 1: Why Event-First Programming Changes Everything
  • Microservices 101: Transactional Outbox and Inbox
  • The Power of Docker Images: A Comprehensive Guide to Building From Scratch

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: