Code readability: Uncover the conditions

In this article I want to show a few techniques that can make you code more expressive and readable especially when dealing with conditions and business rules.

Let’s start with a simple example. Which of these two code snippets makes more sense to you ?

        if (ratio > 13 && ratio < 23) {
            doSomeStuff();
        } else if (ratio <= 13) {
            doOtherStuff();
        }

or
        final boolean ratioInSafeRange = ratio > 13 && ratio < 23;
        final boolean ratioTooLow = ratio <= 13;

        if (ratioInSafeRange) {
            doSomeStuff();
        } else if (ratioTooLow) {
            doOtherStuff();
        }

What are the magic numbers in the first example? Why do we need to do one thing or the other depending on these numbers? Of course, second version does not explain everything. We still must know a bit about the domain to understand this code, but it tells us that there probably is some range of values which is safe, and when the ratio is outside of this range we should act in some different way.

In second version, we gave the conditions theirĀ  names, emphasising their meaning. Some would say that we can simply add comments that will explain what is going on. That’s true, but code changes quickly and comments become outdated and misleading in time. Comments are difficult to maintain and a false comment becomes rather an obstacle than help.

The solution presented above adds more expression to the code itself. It make the comment a part of the code, and therefore makes it more flexible. As the code evolves, developer can use refactoring techniques, bulk rename and so on to keep the comment-in-code up to date.

Now lets take a look at a bit more advanced example. Consider a banking system with the following domain interface:

interface Account {
    public Date creationDate();

    BigDecimal balance();

    BigDecimal avarageMonthlyBalance();

    void debit(BigDecimal amount);

    void credit(BigDecimal amount);
}

We also have a domain service to make fund transfers:
class TransferService {
    public void transfer(final Account source, final Account destination, final BigDecimal amount) {
        source.debit(amount);
        destination.credit(amount);
    }
}

Now the business comes with a new requirement:

For every fund transfer from source account to destination account, a source account should pay a fee, unless the source account fulfills conditions of being a premium account. A premium account is an account which exists at least one year in the system and has an average monthly balance of $1000 or more for the last month.

We decide to put this logic into the TransferService, since the fee is to be paid for transfer and not for debit transaction. Let’s take a look at the first draft:

class TransferService {
    private static final BigDecimal TRANSFER_FEE = BigDecimal.ONE;

    public void transfer(final Account source, final Account destination, final BigDecimal amount) {
        source.debit(amount);
        destination.credit(amount);

        final Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.YEAR, -1);
        if (!(source.creationDate().compareTo(calendar.getTime()) <= 0 && source.avarageMonthlyBalance().compareTo(BigDecimal.valueOf(1000L)) > 0)) {
            source.debit(TRANSFER_FEE);
        }
    }
}

After extracting the date computation into utility class, and applying the named condition pattern, we get the following:
    public void transfer(final Account source, final Account destination, final BigDecimal amount) {
        source.debit(amount);
        destination.credit(amount);

        final boolean accountExistsForAtLeatOneYear = source.creationDate().compareTo(DateTimeUtils.oneYearAgo()) <= 0;
        final boolean monthlyBalanceGreaterThanRequired = source.avarageMonthlyBalance().compareTo(BigDecimal.valueOf(1000L)) > 0;

        if (!(accountExistsForAtLeatOneYear &amp;&amp; monthlyBalanceGreaterThanRequired)) {
            source.debit(TRANSFER_FEE);
        }
    }

Just a small refactoring inside the if statement and here’s the final version of the method:
    public void transfer(final Account source, final Account destination, final BigDecimal amount) {
        source.debit(amount);
        destination.credit(amount);

        final boolean accountExistsForAtLeatOneYear = source.creationDate().compareTo(DateTimeUtils.oneYearAgo()) <= 0;
        final boolean monthlyBalanceGreaterThanRequired = source.avarageMonthlyBalance().compareTo(BigDecimal.valueOf(1000L)) > 0;

        if (!accountExistsForAtLeatOneYear || !monthlyBalanceGreaterThanRequired) {
            source.debit(TRANSFER_FEE);
        }
    }

Much better, isn’t it?

What we achieved is a code which is much more readable and expressive than before. It reveals concepts that were hidden in the first version in a form of complex comparisons and logical operators.

But we can go further. In this case being or not a premium account is a business rule, a specification. It’s not programmers job to define what are the conditions and rules for an account to become premium. If so, lets make it an explicit business rule with a name which we can use talking to business people. When discussing with domain experts we will not talk in terms of IFs, but we can talk in terms of specifications, policies etc.

class PremiumAccountSpecification {

    public boolean isSatisfiedBy(final Account candidate) {
        final boolean accountExistsForAtLeatOneYear = candidate.creationDate().compareTo(DateTimeUtils.oneYearAgo()) <= 0;
        final boolean monthlyBalanceGreaterThanRequired = candidate.avarageMonthlyBalance().compareTo(BigDecimal.valueOf(1000L)) >= 0;

        return accountExistsForAtLeatOneYear &amp;&amp; monthlyBalanceGreaterThanRequired;
    }
}

and the TransferService now looks like this:
    public void transfer(final Account source, final Account destination, final BigDecimal amount) {
        source.debit(amount);
        destination.credit(amount);

        PremiumAccountSpecification premiumAccountSpecification = new PremiumAccountSpecification();
        if (!premiumAccountSpecification.isSatisfiedBy(source)) {
            source.debit(TRANSFER_FEE);
        }
    }

When writing software, we are constantly facing problem of losing important concepts in the code. It is important for the model to hide details behind abstractions, but also to make important things explicit. Business rules are a good example where explicitness pays off. The patterns presented above are useful not only in small scale where they do their job for readability and ease of maintenance of methods and classes, but also in the scale of the domain model, where they are first-class citizens (in form of Specifications) and do their contribution to the domain ubiquitous language.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

Gravatar
WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.