Strategy Factory

We have all heard about design patterns like factory pattern and strategy pattern and design principles like SOLID. If we are asked what is open closed principle we would immediately answer that it is a module, class or functionality in our software which is closed for modifications but open for extensions.

However we often struggle to apply them in real world. We face difficulties to refactor a piece of functionality to use the right patterns or to apply a good software development principle. Honestly it is not an easy job to do so. Is is not only difficult to find out which practices and patterns to use, it is also difficult to decide should we apply any pattern at all or not. Developers often get obsessed with patterns and practices and they try to apply them everywhere they can. We should remember that everything in life is trade off, so is with the patterns and practices. A pattern or practice not implemented correctly may complicate our software and make it hard to maintain. There are lot of factors because of which we can decide not to use design patterns like cost, effort, deadlines, business value and etc.

Patterns and practices are some of our few friends in software development that help us to produce quality software.

In this article we will analyze an example functionality and then we will refactor it to use strategy and factory patterns in order to make it open for extension and closed for modifications. Although for a simple functionality like in this article it may seem overwhelming to use this approach for more complicated and real world application it may be beneficial.

We will look at functionality that calculates a commission for money transactions based on different rules and conditions.

We will create a simple console app for that purpose. To make it look more like a real world software we will also use dependency injection container which we will install as a NuGet package from Unity.Microsoft.DependencyInjection.

Here are our Transaction class and TransactionType enumeration.

    public class Transaction
    {
        public long Id { get; set; }
        public decimal Amount { get; set; }
        public TransactionType TransactionType { get; set; }
        public DateTime? TransactionDateTime { get; set; }
    }

    public enum TransactionType
    {
        Sale,
        Settlement,
        Refund,
        ChargeBack,
        Void
    }

In order to calculate commissions we will create commission entity which ideally should be stored in database but for the sake of simplicity we will create them in memory in this example.

    public class Commission
    {
        public int Id { get; set; }
        public decimal Percent { get; set; }
        public decimal CommissionFixedAmount { get; set; }
        public DateTime ValidUntil { get; set; }
    }

Each commission has a validity date determined by ValidUntil property.

Then we have the following interface for a commission repository which is used to get the active commission based on transaction date.

    public interface ICommissionRepository
    {
        Commission Get(DateTime date);
    }

We will have two implementation of this interface one for regular commissions and one for commissions with transaction type Refund.

    public class CommissionRepository : ICommissionRepository
    {
        // In real app get this information from DB
        private readonly List<Commission> dbCommissions = new List<Commission>
        {
            new Commission
            {
                Id = 1,
                Percent = 10.00m,
                CommissionFixedAmount = 10.00m,
                ValidUntil = new DateTime(2020, 12, 31)
            },
            new Commission
            {
                Id = 2,
                Percent = 20.00m,
                CommissionFixedAmount = 20.00m,
                ValidUntil = new DateTime(2021, 12, 31)
            }
        };

        public Commission Get(DateTime date)
        {
            return dbCommissions.OrderByDescending(c => c.ValidUntil).FirstOrDefault(c => date <= c.ValidUntil);
        }
    }
    public class RefundCommissionRepository: ICommissionRepository
    {
        // In real world get this information from DB
        private readonly List<Commission> dbCommissions = new List<Commission>
        {
            new Commission
            {
                Id = 3,
                Percent = 1.00m,
                CommissionFixedAmount = 1.00m,
                ValidUntil = new DateTime(2020, 12, 31)
            },
            new Commission
            {
                Id = 4,
                Percent = 2.00m,
                CommissionFixedAmount = 2.00m,
                ValidUntil = new DateTime(2021, 12, 31)
            }
        };

        public Commission Get(DateTime date)
        {
            return dbCommissions.OrderByDescending(c => c.ValidUntil).FirstOrDefault(c => date <= c.ValidUntil);
        }
    }

The main functionality will be in CommissionCalculatorService where we will calculate a commission for a given transaction entity.

    public interface ICommissionCalculatorService
    {
        decimal Calculate(Transaction transaction);
    }
    public class CommissionCalculatorService : ICommissionCalculatorService
    {
        private readonly ICommissionRepository commissionRepository;
        private readonly ICommissionRepository refundCommissionRepository;

        // In real world read those values from config or database
        private const decimal DefaultCommissionPercent = 5.00m;
        private const decimal BeforeScheduleCommissionPercent = 1.00m;
        private readonly DateTime commissionScheduleStartDate = new DateTime(2000, 1, 1);

        public CommissionCalculatorService(List<ICommissionRepository> commissionRepositories)
        {
            this.commissionRepository =
                commissionRepositories.First(cr => cr.GetType() == typeof(CommissionRepository));

            this.refundCommissionRepository =
                commissionRepositories.First(cr => cr.GetType() == typeof(RefundCommissionRepository));
        }

        public decimal Calculate(Transaction transaction)
        {
            if (transaction == default(Transaction))
            {
                return 0.00m;
            }

            if (transaction.TransactionType == TransactionType.ChargeBack ||
                transaction.TransactionType == TransactionType.Void)
            {
                return 0.00m;
            }

            if (!transaction.TransactionDateTime.HasValue)
            {
                return transaction.Amount * DefaultCommissionPercent / 100;
            }

            if (transaction.TransactionDateTime <= this.commissionScheduleStartDate)
            {
                return transaction.Amount * BeforeScheduleCommissionPercent / 100;
            }

            if (transaction.TransactionType == TransactionType.Refund)
            {
                var refundCommission = this.refundCommissionRepository.Get(transaction.TransactionDateTime.Value);
                if (refundCommission == null)
                {
                    return -1 * (transaction.Amount * DefaultCommissionPercent / 100);
                }

                return -1 * (transaction.Amount * refundCommission.Percent / 100 + refundCommission.CommissionFixedAmount);
            }

            var commission = this.commissionRepository.Get(transaction.TransactionDateTime.Value);
            if (commission == null)
            {
                return transaction.Amount * DefaultCommissionPercent / 100;
            }

            return transaction.Amount * commission.Percent / 100 + commission.CommissionFixedAmount;

        }
    }

Take a moment to examine the logic for calculating the commission which depends on different properties of transaction entity.

We have the following rules for calculating commission:

  • If transaction is null or it’s type is ChargeBack or Void then no commission is calculated
  • If transaction does not have a TransactionDateTime then calculate default commission
  • If transaction date is before commissionScheduleStartDate then calculate a commission based on BeforeScheduleCommissionPercent
  • If transaction type is Refund then get the valid commission from RefundCommissionRepository and calculate the commission based on negative refund commission, however if there is no valid refund commission then calculate a negative default commission.
  • If transaction type is Sale or Settlement then get the valid commission from CommissionRepository and calculate the commission based on retrieved commission, however if there is no valid commission then calculate default commission

This is the area that we are going to refactor. Imagine we start receiving more and more rules for calculating transaction commissions then each time we have to open this logic and modify it and at some point it may become really hard to understand and maintain with all those if statements and conditions.

Let’s see how we calculate commissions for transactions with different properties.

        static void Main(string[] args)
        {
            var container = new UnityContainer();

            container.RegisterType<ICommissionCalculatorService, CommissionCalculatorService>();
            container.RegisterType<ICommissionRepository, CommissionRepository>("Default");
            container.RegisterType<ICommissionRepository, RefundCommissionRepository>("Refund");

            var commissionCalculatorService = container.Resolve<ICommissionCalculatorService>();

            List<Transaction> transactions = new List<Transaction>
            {
                null,
                new Transaction
                {
                    Id = 1,
                    Amount = 100m,
                    TransactionType = TransactionType.ChargeBack,
                    TransactionDateTime = new DateTime(2000, 2, 2)
                },
                new Transaction
                {
                    Id = 2,
                    Amount = 100m,
                    TransactionType = TransactionType.Void,
                    TransactionDateTime = DateTime.Now
                },
                new Transaction
                {
                    Id = 3,
                    Amount = 100m,
                    TransactionType = TransactionType.Sale
                },
                new Transaction
                {
                    Id = 4,
                    Amount = 100m,
                    TransactionType = TransactionType.Sale,
                    TransactionDateTime = new DateTime(1999,12,15)
                },
                new Transaction
                {
                    Id = 5,
                    Amount = 100m,
                    TransactionType = TransactionType.Refund,
                    TransactionDateTime = new DateTime(2021,8,15)
                },
                new Transaction
                {
                    Id = 6,
                    Amount = 100m,
                    TransactionType = TransactionType.Refund,
                    TransactionDateTime = new DateTime(2022,8,15)
                },
                new Transaction
                {
                    Id = 7,
                    Amount = 100m,
                    TransactionType = TransactionType.Sale,
                    TransactionDateTime = new DateTime(2021,8,15)
                },
                new Transaction
                {
                    Id = 8,
                    Amount = 100m,
                    TransactionType = TransactionType.Sale,
                    TransactionDateTime = new DateTime(2022,8,15)
                },
                new Transaction
                {
                    Id = 9,
                    Amount = 100m,
                    TransactionType = TransactionType.Settlement,
                    TransactionDateTime = new DateTime(2021,8,15)
                },
                new Transaction
                {
                    Id = 10,
                    Amount = 100m,
                    TransactionType = TransactionType.Settlement,
                    TransactionDateTime = new DateTime(2022,8,15)
                }
            };

            foreach (var transaction in transactions)
            {
                var commission = commissionCalculatorService.Calculate(transaction);
                Console.WriteLine($"Commission for transaction with Id:{transaction?.Id} and amount:{transaction?.Amount} is {commission}");
            }

        }

We get the following commissions calculated

Refactoring to strategy factory

Imagine the logic for each commission calculation is separated and encapsulated in it’s own class/module.

By doing this we impose a single responsibility principle and also separation of concerns if you like. When we have an issue in a specific commission calculation we open one class and look for the problem there instead of struggling in a code with a lot of ifs and switches.

Also if we have a new commission calculation rule we won’t modify any existing strategy but we will add new class with the new calculation logic. It sounds like an open closed principle.

But who will decide which calculation strategy is going to be used. Here comes the factory, which will orchestrate the different strategies and return the correct strategy to calculate transaction commission.

Let’s start creating strategies for different commission calculations.

    public class NoCommissionCalculator: ICommissionCalculatorService
    {
        public decimal Calculate(Transaction transaction)
        {
            return 0.00m;
        }
    }

This strategy will be used in cases where we have no commission.

    public class DefaultCommissionCalculator: ICommissionCalculatorService
    {
        // In real world read this value from config or database
        private const decimal DefaultCommissionPercent = 5.00m;

        public decimal Calculate(Transaction transaction)
        {
            return transaction.Amount * DefaultCommissionPercent / 100;
        }
    }

This strategy is for default commission calculation.

    public class BeforeScheduleCommissionCalculator : ICommissionCalculatorService
    {
        // In real world read this value from config or database
        private const decimal BeforeScheduleCommissionPercent = 1.00m;

        public decimal Calculate(Transaction transaction)
        {
            return transaction.Amount * BeforeScheduleCommissionPercent / 100;
        }
    }

This strategy is for cases where transaction date is earlier than CommissionScheduleStartDate.

    public class RefundCommissionCalculator : ICommissionCalculatorService
    {
        private readonly ICommissionRepository refundCommissionRepository;
        private readonly ICommissionCalculatorService defaultCommissionCalculatorService;

        public RefundCommissionCalculator(
            IEnumerable<ICommissionRepository> commissionRepositories,
            IEnumerable<ICommissionCalculatorService> commissionCalculatorServices)
        {
            this.refundCommissionRepository = commissionRepositories.First(cr => cr.GetType() == typeof(RefundCommissionRepository));

            this.defaultCommissionCalculatorService = commissionCalculatorServices.First(cc=>cc.GetType() == typeof(DefaultCommissionCalculator));
        }

        public decimal Calculate(Transaction transaction)
        {
            var refundCommission = this.refundCommissionRepository.Get(transaction.TransactionDateTime.Value);
            if (refundCommission == null)
            {
                return -1 * this.defaultCommissionCalculatorService.Calculate(transaction);
            }

            return -1 * (transaction.Amount * refundCommission.Percent / 100 + refundCommission.CommissionFixedAmount);
        }
     }

This is the strategy for refund commission calculation. We inject the default commission calculator here as it is used to calculate default commission if we don’t find valid refund commission.

    public class MainCommissionCalculator : ICommissionCalculatorService
    {
        private readonly ICommissionRepository commissionRepository;
        private readonly ICommissionCalculatorService defaultCommissionCalculatorService;

        public MainCommissionCalculator(
            IEnumerable<ICommissionRepository> commissionRepositories,
            IEnumerable<ICommissionCalculatorService> commissionCalculatorServices)
        {
            this.commissionRepository = commissionRepositories.First(cr => cr.GetType() == typeof(CommissionRepository));

            this.defaultCommissionCalculatorService = commissionCalculatorServices.First(cc=>cc.GetType() == typeof(DefaultCommissionCalculator));
        }

        public decimal Calculate(Transaction transaction)
        {
            var commission = this.commissionRepository.Get(transaction.TransactionDateTime.Value);
            if (commission == null)
            {
                return this.defaultCommissionCalculatorService.Calculate(transaction);
            }

            return transaction.Amount * commission.Percent / 100 + commission.CommissionFixedAmount;
        }
    }

This is the strategy for main commission calculation for transaction types Sale and Settlement. We inject the default commission calculator here as it is used to calculate default commission if we don’t find valid commission.

Now let’s look at the factory class that will orchestrate all those strategies and return the correct commission calculator.

    public interface ICommissionCalculatorFactory
    {
        ICommissionCalculatorService CreateFor(Transaction transaction);
    }
    public class CommissionCalculatorFactory : ICommissionCalculatorFactory
    {
        // In real world read this value from config or database
        private static readonly DateTime CommissionScheduleStartDate = new DateTime(2000, 1, 1);

        private readonly IEnumerable<ICommissionCalculatorService> commissionCalculatorServices;

        private readonly Func<Transaction, bool> isNoCommission = transaction =>
            transaction == default(Transaction) || transaction.TransactionType == TransactionType.ChargeBack || transaction.TransactionType == TransactionType.Void;

        private readonly Func<Transaction, bool> isDefaultCommission = transaction => !transaction.TransactionDateTime.HasValue;

        private readonly Func<Transaction, bool> isBeforeScheduleCommission = transaction => transaction.TransactionDateTime <= CommissionScheduleStartDate;

        private readonly Func<Transaction, bool> isRefundCommission = transaction => transaction.TransactionType == TransactionType.Refund;

        private readonly Func<Transaction, bool> isMainCommission = transaction => true;


        public CommissionCalculatorFactory(IEnumerable<ICommissionCalculatorService> commissionCalculatorServices)
        {
            this.commissionCalculatorServices = commissionCalculatorServices;
        }

        public ICommissionCalculatorService CreateFor(Transaction transaction)
        {
            Dictionary<Func<Transaction, bool>, ICommissionCalculatorService> commissionCalculators =
                new Dictionary<Func<Transaction, bool>, ICommissionCalculatorService>
                {
                    { this.isNoCommission, this.commissionCalculatorServices.First(cs => cs.GetType() == typeof(NoCommissionCalculator)) },
                    { this.isDefaultCommission, this.commissionCalculatorServices.First(cs => cs.GetType() == typeof(DefaultCommissionCalculator)) },
                    { this.isBeforeScheduleCommission, this.commissionCalculatorServices.First(cs => cs.GetType() == typeof(BeforeScheduleCommissionCalculator)) },
                    { this.isRefundCommission, this.commissionCalculatorServices.First(cs => cs.GetType() == typeof(RefundCommissionCalculator)) },
                    { this.isMainCommission, this.commissionCalculatorServices.First(cs => cs.GetType() == typeof(MainCommissionCalculator)) }
                };

            return commissionCalculators.First(cc => cc.Key(transaction)).Value;
        }
    }

In the factory class we define all conditions as delegates and then construct a key value collection where the key is the delegate and the value is the corresponding commission calculator strategy.

We then simply return the commission calculator whose delegate returns true.

Let’s once again execute the main function and see how a commission is calculated for each transaction from our previous example above before the refactoring.

        static void Main(string[] args)
        {
            var container = new UnityContainer();

            container.RegisterType<ICommissionCalculatorService, CommissionCalculatorService>();
            container.RegisterType<ICommissionRepository, CommissionRepository>("Default");
            container.RegisterType<ICommissionRepository, RefundCommissionRepository>("Refund");

            container.RegisterType<ICommissionCalculatorFactory, CommissionCalculatorFactory>();

            container.RegisterType<ICommissionCalculatorService, NoCommissionCalculator>("NoCommissionCalculator");
            container.RegisterType<ICommissionCalculatorService, DefaultCommissionCalculator>("DefaultCommissionCalculator");
            container.RegisterType<ICommissionCalculatorService, BeforeScheduleCommissionCalculator>("BeforeScheduleCommissionCalculator");
            container.RegisterType<ICommissionCalculatorService, RefundCommissionCalculator>("RefundCommissionCalculator");
            container.RegisterType<ICommissionCalculatorService, MainCommissionCalculator>("MainCommissionCalculator");

            var commissionCalculatorFactory = container.Resolve<CommissionCalculatorFactory>();

            List<Transaction> transactions = new List<Transaction>
            {
                null,
                new Transaction
                {
                    Id = 1,
                    Amount = 100m,
                    TransactionType = TransactionType.ChargeBack,
                    TransactionDateTime = new DateTime(2000, 2, 2)
                },
                new Transaction
                {
                    Id = 2,
                    Amount = 100m,
                    TransactionType = TransactionType.Void,
                    TransactionDateTime = DateTime.Now
                },
                new Transaction
                {
                    Id = 3,
                    Amount = 100m,
                    TransactionType = TransactionType.Sale
                },
                new Transaction
                {
                    Id = 4,
                    Amount = 100m,
                    TransactionType = TransactionType.Sale,
                    TransactionDateTime = new DateTime(1999,12,15)
                },
                new Transaction
                {
                    Id = 5,
                    Amount = 100m,
                    TransactionType = TransactionType.Refund,
                    TransactionDateTime = new DateTime(2021,8,15)
                },
                new Transaction
                {
                    Id = 6,
                    Amount = 100m,
                    TransactionType = TransactionType.Refund,
                    TransactionDateTime = new DateTime(2022,8,15)
                },
                new Transaction
                {
                    Id = 7,
                    Amount = 100m,
                    TransactionType = TransactionType.Sale,
                    TransactionDateTime = new DateTime(2021,8,15)
                },
                new Transaction
                {
                    Id = 8,
                    Amount = 100m,
                    TransactionType = TransactionType.Sale,
                    TransactionDateTime = new DateTime(2022,8,15)
                },
                new Transaction
                {
                    Id = 9,
                    Amount = 100m,
                    TransactionType = TransactionType.Settlement,
                    TransactionDateTime = new DateTime(2021,8,15)
                },
                new Transaction
                {
                    Id = 10,
                    Amount = 100m,
                    TransactionType = TransactionType.Settlement,
                    TransactionDateTime = new DateTime(2022,8,15)
                }
            };

            foreach (var transaction in transactions)
            {
                var commissionCalculator = commissionCalculatorFactory.CreateFor(transaction);
                var commission = commissionCalculator.Calculate(transaction);
                Console.WriteLine($"Commission for transaction with Id:{transaction?.Id} and amount:{transaction?.Amount} is {commission}");
            }
        }

You can see how we first register all strategies and then for each transaction we ask for a specific commission calculator and then calculate the commission. As expected the result is same as above.

We have refactored our logic to use different strategies for different commission calculations using patterns and principles like strategy, factory, open closed, separation of concerns and single responsibility.

Leave a Reply

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

WordPress.com Logo

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

Facebook photo

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

Connecting to %s