How to Reduce Cyclomatic Complexity Part 9: Switchable Factory Methods

by Zoran Horvat

In this series of articles we have been discussing methods that can be applied to reduce cyclomatic complexity of code. Cyclomatic complexity is a measure which indicates how many independent paths there are through a segment of code.

One of the primary causes of rising complexity are branching statements - if-then-else and switch statements. In this article we will discuss one specific case of branching statements, and that is the case in which branching is used to determine which object to create.

Principal Example

Take a look at this classic example. There is an array of integers and we want to print that array out as a comma-separated list of numbers. The point about this exercise is in the separator placed between numbers:

void Print(int[] array)
{
    for (int i = 0; i < array.Length; i++)
    {
        Console.Write(array[i]);
        if (i < array.Length - 1)
            Console.Write(", ");
        else
            Console.WriteLine();
    }
}

In this implementation, we are using comma and a space in all cases except when the last element of the array is printed out. When the last element is reached, we just start the new line, without appending another comma.

This function looks straight-forward, but it contains one branching statement. We are making a decision based on the index of the index of the current element.

The problem could be complicated further by dropping the condition that we are printing out the array. We could just have an enumeration at hand:

void Print(IEnumerable<int> array)
{
    foreach (int number in array)
    {
        ...
    }
}

In this case there is no "last" element. Inside the loop, we are oblivious of the collection's length. Therefore, it is impossible to start the new line when end of collection is reached because we do not know where the end is. But we can turn the tables and act upon the first element instead:

void Print(IEnumerable<int> array)
{

    bool isFirst = true;

    foreach (int number in array)
    {
        if (isFirst)
            isFirst = false;
        else
            Console.Write(", ");
        Console.Write(number);
    }

    Console.WriteLine();

}

This time, the loop is sensitive to the first element indicator. Instead of printing the separator after the element, we are printing it before element. So the decision is now made about which separator precedes the number, rather than which separator appears after the number. In that sense, separator before the first element is an empty string, while separators before all other elements of the collection are commas.

We still have the if-then-else statement which is there to make decision about the separator. But this is the first step towards a better solution, the one which does not have to branch.

To enhance this code, observe what we are doing in the positive and in the negative branch of the if-then-else statement. Then branch clears the isFirst flag and prints nothing. Else branch prints comma and a space.

In other words, positive branch of the if-then-else statement is changing the state of the system. We could also suspect that it is printing an empty string. (There is no way to prove the opposite when looking at the console output!) Negative branch is then there to just print the separator. Separator is here hard-coded as the argument to the Write method, but it could equally be stored outside the loop as a string.

These observation lead to a completely different and much simpler implementation:

void Print(IEnumerable<int> array)
{

    string separator = string.Empty;

    foreach (int number in array)
    {
        Console.Write("{0}{1}", separator, number);
        separator = ", ";
    }

    Console.WriteLine();

}

The isFirst flag is gone, replaced by a string. That string is the separator, but it also acts as the flag. The first time we pass the loop, the flag is turned into something else. That is exactly the same thing that happened to the isFirst flag before.

Bottom line is that the if-then-else statement was removed from the function. It is not required because data that we use in the loop, such as the separator, can be used to piggyback control information, such as the flag used to branch execution.

Generalizing the Solution

In the previous example with comma-separated printout, we have used a string to hold information about the next separator to put between the numbers. This solution is quite satisfactory to the problem at hand. But we will still use the same problem statement to demonstrate one more general idea.

How would it go if we used a function to calculate a separator? Here is how we would begin with this idea:

void Print(IEnumerable<int> array)
{

    Func<string> getSeparator = () => string.Empty;

    foreach (int number in array)
    {
        ...
    }

    Console.WriteLine();

}

But this solution is not complete - all separators would be empty strings, but we have to put a comma in all the cases except the first one. To fix the issue, we will have to change the factory function for the subsequent passes:

void Print(IEnumerable<int> array)
{

    Func<string> getSeparator = () =>
        {
            getSeparator = () => ", ";
            return string.Empty;
        };

    foreach (int number in array)
    {
        Console.Write("{0}{1}", getSeparator(), number);
    }

    Console.WriteLine();

}

This time, only the first call to the getSeparator method will return an empty string. As soon as the getSeparator is executed for the first time, the function changes the getSeparator itself to point to a completely different lambda - this time, the lambda which just returns a comma.

Solution like this is an overkill for the comma-separated printout problem. But it can turn out to be of higher value in some more complicated cases.

Real World Example

In this series of articles we were refactoring one simple e-commerce application. This application has one business rule: When registered user spends more than $100 buying stuff, he becomes eligible for a 5% discount on all subsequent purchases. This requirement is implemented in the User class like this:

namespace Store.Domain.Implementation
{
    public class User: IRegisteredUser
    {
        private decimal totalPurchases;
        private bool hasReceivedLoyaltyDiscount;
        private IList<IDiscount> discounts = new List<IDiscount>();
        ...
        private void RegisterPurchase(decimal price)
        {
            this.totalPurchases += price;
            if (!hasReceivedLoyaltyDiscount && this.totalPurchases > 100.0M)
            {
                this.discounts.Add(new Discount(0.05M));
                this.hasReceivedLoyaltyDiscount = true;
            }
        }
        ...
    }
}

Whenever a User object is used to buy something, domain service calls the RegisterPurchase on that User object so that it can add a newly acquired discount to the list of discounts.

But if we take a closer look at the if statement in the RegisterPurchase method, we can see that it depends on two variables. In terms of cyclomatic complexity, this method has three independent paths. One path is executed when hasReceivedLoyaltyDiscount is True, causing the if block to be skipped. Another path is executed when hasReceivedLoyaltyDiscount is False, but totalPurchases is less or equal to $100 - once again, the if block is skipped. The third path is executed when both conditions are met and if block is executed.

We could reduce complexity of this method in the same way as we did with the comma-separated array printing. In case of the discount, we have the same situation. Discount must be assigned only once. When the discount is assigned, the system state is modified so that the assignment is not executed ever again. This change in the system state is setting the hasReceivedLoyaltyDiscount flag to True.

Factory Methods Instead of Branching

Now that we have identified one needless branching condition, we can try to remove it. The first step will be to isolate the decision to create the loyalty discount:

namespace Store.Domain.Implementation
{
    public class User: IRegisteredUser
    {
        ...
        private Option<IDiscount> TryCreateLoyaltyDiscount()
        {
            if (this.totalPurchases > 100.0M)
                return Option<IDiscount>.Create(new Discount(0.05M));
            return Option<IDiscount>.CreateEmpty();
        }
    }
}

Once again, we see our old friend Option<T> functional type . This method creates the discount if condition is met. Otherwise, if requested amount is not fulfilled yet, this method just returns an empty Option.

Notice that cyclomatic complexity of this method is 2. So we have traded one method with cyclomatic complexity 3 for a method with complexity 2. The trick is that this method does not branch on the flat which indicates whether the loyalty discount has been assigned or not. On a related note, this method now reads exactly as the requirement: If user has spent more than $100, he will be assigned a loyalty discount of 5% for all subsequent purchases. Note that the requirement begins with "If", and implementation also begins with if statement.

That is one situation in which branching can be justified. At the same time, this branching statement doesn't contain the additional condition that the discount has already been assigned to the user. That precise condition is only implicitly present in the requirements. Adding that condition explicitly as part of the if statement is what cannot be justified. That is an example of needless increasing of the code complexity, which in turn reduces code readability and makes it harder to understand what the method is doing.

Back to the problem. The next method we will add is the one which wraps call to the TryCreateLoyaltyDiscount:

namespace Store.Domain.Implementation
{
    public class User: IRegisteredUser
    {
        ...
        private void TryAssignLoyaltyDiscount()
        {
            this.TryCreateLoyaltyDiscount()
                .Each(discount => this.discounts.Add(discount));
        }

        private Option<IDiscount> TryCreateLoyaltyDiscount()
        {
            if (this.totalPurchases > 100.0M)
                return Option<IDiscount>.Create(new Discount(0.05M));
            return Option<IDiscount>.CreateEmpty();
        }
    }
}

TryAssignLoyaltyDiscount method is using the TryCreateLoyaltyDiscount method to obtain a discount option - a collection with zero or one discount in it. Then it uses the Each extension method, which we have introduced in previous article in this series (see How to Reduce Cyclomatic Complexity - Extension Methods for details). Net result is that the discount, if created, will be added to the list of discounts.

Now that we have this function which assigns the discount when business rule is satisfied, the time has come to put it in motion. Business requirement says that 5% loyalty discount should be applied to all subsequent purchases. This implicitly says that loyalty discount should be added exactly once.

To implement this requirement, we will use the factory method for loyalty discounts:

namespace Store.Domain.Implementation
{
    public class User: IRegisteredUser
    {
        public string UserName { get; private set; }
        private decimal totalPurchases;
        private IList<IDiscount> discounts = new List<IDiscount>();
        private IBuyer referrer;
        private Func<Option<IDiscount>> tryCreateLoyaltyDiscount;

        public User(string userName)
        {
            this.UserName = userName;
            this.tryCreateLoyaltyDiscount = () => this.CreateLoyaltyDiscountIfFulfilled();
        }
        ...
        private void TryAssignLoyaltyDiscount()
        {
            this.tryCreateLoyaltyDiscount()
                .Each(discount => this.discounts.Add(discount));
        }

        private Option<IDiscount> CreateLoyaltyDiscountIfFulfilled()
        {
            if (this.totalPurchases > 100.0M)
                return Option<IDiscount>.Create(new Discount(0.05M));
            return Option<IDiscount>.CreateEmpty();
        }
    }
}

Loyalty discount is now created using a factory method, which is defined as a lambda function. Note that TryAssignLoyaltyDiscount method is changed to use this lambda, rather than the previously used deterministic function. Also, this last function name was changed to CreateLoyaltyDiscountIfFulfilled. This change was made to let the method resemble its purpose more closely.

Final step is to complete the TryAssignLoyaltyDiscount. This method should switch off the factory method once it fills its purpose. This is the complete implementation which assigns 5% loyalty discount once the user has spent $100 buying:

namespace Store.Domain.Implementation
{
    public class User: IRegisteredUser
    {
        public string UserName { get; private set; }
        private decimal totalPurchases;
        private IList<IDiscount> discounts = new List<IDiscount>();
        private IBuyer referrer;
        private Func<Option<IDiscount>> tryCreateLoyaltyDiscount;

        public User(string userName)
        {
            this.UserName = userName;
            this.tryCreateLoyaltyDiscount = () => this.CreateLoyaltyDiscountIfFulfilled();
        }
        ...
        private void RegisterPurchase(decimal price)
        {
            this.totalPurchases += price;
            this.TryAssignLoyaltyDiscount();
        }

        private void TryAssignLoyaltyDiscount()
        {
            this.tryCreateLoyaltyDiscount()
                .Each(discount =>
                    {
                        this.discounts.Add(discount);
                        this.tryCreateLoyaltyDiscount = () => Option<IDiscount>.CreateEmpty();
                    });
        }

        private Option<IDiscount> CreateLoyaltyDiscountIfFulfilled()
        {
            if (this.totalPurchases > 100.0M)
                return Option<IDiscount>.Create(new Discount(0.05M));
            return Option<IDiscount>.CreateEmpty();
        }
    }
}

Analysis

Drawback of this technique is that it is a bit cryptic and not that easy to figure the first time. Therefore it is probably not the best option in simple cases.

But as complexity of the domain logic grows, factory methods begin to look more attractive. The trick is that all these methods have the same structure. They perform the operation and then modify the lambda for the next call. Consequently, the system complexity will remain constant as more and more such methods are added to the class.

Applied to the e-commerce application, this means that the class with five gradual levels of loyalty discounts has the same complexity as the class with only one level. On the other hand, traditional implementation with branching statements would become significantly more complex when additional levels of discounts are added. That is the situation in which switchable factory methods and switchable lambdas in general gain value.

Conclusion

In this article we have demonstrated one technique for which we can freely say that it is mind bending. Instead of branching the code to decide which object to create at the given situation, we use factory methods. But, these are not ordinary factory methods. These methods are dynamically produced using lambda expressions.

And right there comes the mind bending moment. Lambdas which produce objects also change themselves! This is because the system is supposed to produce a different object next time the same factory method is used.

Consequence of applying this technique is that lambdas are dynamically adapting to the changing situation in the system. Explicit branching is not required anymore. That is how we can reduce cyclomatic complexity of our code.


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