How to Reduce Cyclomatic Complexity Part 6: Functional Domain Logic

by Zoran Horvat

In previous article we were discussing the use of Option<T> functional type in cases when there is no object on which we could operate. Option is a collection - it implements IEnumerable interface - but it can only contain zero or one element. See How to Reduce Cyclomatic Complexity: Option<T> Functional Type for more details.

On one hand, option lets us produce non-null result even when there is no object that could be the proper result. We just produce an empty option and in that way we indicate that there is no object. But Option also lets us implement uniform logic over both cases - when there is and when there is no object on which the operation is supposed to be executed. To accomplish that, we rely on the LINQ to Objects library, which is pretty much functional in its own design.

In this article, we will push the use of functional logic one big step forward. We will refactor the domain model so that it encompasses series of calls to functions, rather than control constructs such as for loops or if-then-else statements. That will help the domain model become much easier to understand and to code.

Return Option Instead of Null

We have already seen in the Option<T> Functional Type article that methods can return Option<T> object instead of null reference when there is no object to return. In this article we will push the same idea further. First, consider the way in which repository returns User object when domain service looks up the user:

namespace Store.Infrastructure
{
    public class UserRepository: IUserRepository
    {
        ...
        public User Find(string userName)
        {
            User user = null;
            if (this.userNameToUser.TryGetValue(userName, out user))
                return user;
            return null;
        }
    }
}

As you can see, the Find method of the UserRepository may return null in case when the requested user cannot be found. This repository only simulates the real situation and it keeps all the registered users in a dictionary. But it is easy to imagine the same situation with full-blown database. When specified record cannot be found in the database table, this repository would just return null result.

Now let’s see what the caller of this Find method does:

namespace Store.Domain.Implementation
{
    public class DomainServices: IDomainServices
    {
        ...
        public bool VerifyCredentials(string userName)
        {
            return this.userRepository.Find(userName) != null;
        }

        public IReceiptViewModel Purchase(string userName, string itemName)
        {

            IRegisteredUser user = this.userRepository.Find(userName);
            if (user == null)
                return new InvalidUser(userName);
            IAccount account = this.accountRepository.FindByUser(user);

            return this.Purchase(user, account, itemName);

        }
        ...
    }
}

This is the domain service. In the first method, VerifyCredentials, only checks if the specified user exists in the repository. (Once again, this is just simulation so we are not bothering with the user’s password.)

But the second method, Purchase, is more interesting. It picks the user and then forwards the call for further processing. But if the user cannot be found, the method gives up and returns InvalidUser response, which is the application of the Null Object pattern .

Let’s see what we can get when the repository returns Option<User> instead of the nullable User object. Here is the repository implementation:

namespace Store.Infrastructure
{
    public class UserRepository: IUserRepository
    {
        ...
        public Option<User> TryFind(string userName)
        {
            User user = null;
            if (this.userNameToUser.TryGetValue(userName, out user))
                return Option<User>.Create(user);
            return Option<User>.CreateEmpty();
        }
    }
}

After this change, repository always returns non-null result. Only sometimes, when finding fails, the Option returned will be empty. Notice a slight change in the method name. Now it is called TryFind, which indicates that calling this method might not end up with having the User object in your hand.

This is the small change in terms of the repository implementation, but it has a profound effect on the domain service. This time, domain service can be implemented by mapping the collection of users to the collection of purchase receipts:

namespace Store.Domain.Implementation
{
    public class DomainServices: IDomainServices
    {
        ...
        public bool VerifyCredentials(string userName)
        {
            return this.userRepository.TryFind(userName).Any();
        }

        public IReceiptViewModel Purchase(string userName, string itemName)
        {
            return  this.userRepository
                .TryFind(userName)
                .Select(user => this.Purchase(user, itemName))
                .DefaultIfEmpty(new InvalidUser(userName))
                .Single();
        }

        private IReceiptViewModel Purchase(User user, string itemName)
        {
            IAccount account = this.accountRepository.FindByUser(user);
            return this.Purchase(user, account, itemName);
        }
        ...
    }
}

VerifyCredentials method did not change much. Instead of testing the result against null, we are just using the LINQ extension method Any.

But the Purchase method is now quite different. This method maps the user to the receipt. If the collection of users (Option) returned by the user repository was empty, Select method would do nothing. There will be no purchase on the user object because there is no user object. Case of empty user collection is covered by the DefaultIfEmpty call. That method provides alternative result in case when there is no user. Finally, we have to call the Single method to isolate the receipt that will be returned from the Purchase method.

This is quite different way of writing domain logic. Instead of branching depending on conditions, we are solving both cases in one stream of function calls. There is no branching. All cases are the same to us.

Using Option in Methods Returning Void

Take a look at the Deposit method of the domain services class:

namespace Store.Domain.Implementation
{
    public class DomainServices: IDomainServices
    {
        ...
        public void Deposit(string userName, decimal amount)
        {
            User user = this.userRepository.Find(userName);
            if (user != null)
                this.accountRepository.FindByUser(user).Deposit(amount);
        }

    }
}

It uses the same branching logic to decide whether to proceed with the deposit or to skip. With the TryFind method of the user repository which returns Option, we can rewrite this piece of code:

namespace Store.Domain.Implementation
{
    public class DomainServices: IDomainServices
    {
        ...
        public void Deposit(string userName, decimal amount)
        {
            this.userRepository
                .TryFind(userName)
                .Select(user => this.accountRepository.FindByUser(user))
                .ToList()
                .ForEach(account => account.Deposit(amount));
        }

    }
}

Once again, branching has disappeared.

More Examples

We could apply the same process to the product repository. That is the class which holds a collection of available products.

namespace Store.Infrastructure
{
    public class ProductRepository: IProductRepository
    {
        public Product Find(string itemName)
        {
            if (itemName.Length < 10)
                return new Product(itemName);
            return null;
        }
    }
}

Product repository was used by the domain services to locate the product before purchasing it:

namespace Store.Domain.Implementation
{
    public class DomainServices: IDomainServices
    {
        ...
        private IReceiptViewModel Purchase(IBuyer buyer, IAccount account, string itemName)
        {

            Product item = this.productRepository.Find(itemName);

            if (item == null)
                return new OutOfStock(buyer, itemName);

            IMoneyTransaction transaction = account.Withdraw(item.Price);

            if (transaction == null)
                return new InsufficientFunds(buyer, item.Price, itemName);

            return buyer.Purchase(item);

        }
        ...
    }
}

Once again, we can see quite a lot of control logic in this method. First, product might not exist, which causes OutOfStock result to be returned. Further down the stream, user’s account might not be able to produce requested amount of money, which results in the InsufficientFund object being returned.

Now instead of the Find method in the product repository, which potentially returns null reference, we can design a TryFind method which returns Option<Product>:

namespace Store.Infrastructure
{
    public class ProductRepository: IProductRepository
    {
        public Option<Product> TryFind(string itemName)
        {
            if (itemName.Length < 10)
                return Option<Product>.Create(new Product(itemName));
            return Option<Product>.CreateEmpty();
        }
    }
}

Also, user’s account might implement TryWithdraw method which also returns Option to indicate that the monetary transaction might not be possible:

namespace Store.Domain.Implementation
{
    public class Account: IAccount
    {
        ...
        public Option<IMoneyTransaction> TryWithdraw(decimal amount)
        {

            if (this.Balance < amount)
                return Option<IMoneyTransaction>.CreateEmpty();

            MoneyTransaction transaction = new MoneyTransaction(-amount);
            this.transactions.Add(transaction);

            this.Log(string.Format("{0} withdrew ${1:0.00} - balance ${2:0.00}",
                                    this.UserName, amount, this.Balance));

            return Option<IMoneyTransaction>.Create(transaction);

        }
        ...
    }
}

With these changes in place, we can refactor the Purchase method of the domain service to become much simpler and easier to follow:

namespace Store.Domain.Implementation
{
    public class DomainServices: IDomainServices
    {
        ...
        private IReceiptViewModel Purchase(IBuyer buyer, IAccount account, string itemName)
        {
            return this.productRepository
                .TryFind(itemName)
                .Select(item => this.Purchase(buyer, account, item))
                .DefaultIfEmpty(new OutOfStock(buyer, itemName))
                .Single();
        }

        private IReceiptViewModel Purchase(IBuyer buyer, IAccount account, Product item)
        {
            return account.TryWithdraw(item.Price)
                .Select(transaction => buyer.Purchase(item))
                .DefaultIfEmpty(new InsufficientFunds(buyer, item.Price, item.Name))
                .Single();
        }
        ...
    }
}

As you can see, domain logic has once again became straight-forward. There is no branching, complete flow is executed in one go.

Conclusion

In this article we have used collections of objects instead of objects themselves to remove branching. It is possible to implement complete domain logic by mapping one object into another. LINQ to Objects library offers extensive mapping capabilities suitable for this use. By mapping objects instead of branching and looping through them, we are greatly simplifying the domain logic.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub