Single Responsibility Pattern

In my quest for effortful study and learning new skills in software development, I purchased the Robert C. Martin Series book “Agile Principles, Patterns and Practices in C#” which I must say contains effulgent content every C# developer needs to know. In the past I have struggled with grasping software practice principles but this book really helps me in lessening that burden by providing succinct, clear and solid introduction to various agile methods – both coding principles as well as agile processes. Among these principles, one seems very simple but is hard to get right (as Robert C. Martin puts it). It’s the first principle of SOLID (Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion, a mnemonic acronym orchestrated by Michael Feathers for the “first five principles” identified by Robert C. Martin) and also one of the oldest principles of software development: the Single Responsibility Principle (SRP). It states that there should never be more than one reason for a class to change. That is, every class should have only one responsibility  Everything in the class should be related to that single purpose. It does not mean that your classes should only contain one method or property. There may be many members as long as they relate to the single responsibility. It may be that when the one reason to change occurs, multiple members of the class may need modification. It may also be that multiple classes will require updates.

Example Code

To demonstrate the application of the SRP, we consider an Order class that exposes a problem with too many responsibilities thereby violating the SRP and explain how it can be refactored to comply with the principle:


    public class Order
    {
        public void Checkout(Cart cart, PaymentDetails paymentDetails, bool notifyCustomer)
        {
            if (paymentDetails.PaymentMethod == PaymentMethod.CreditCard)
            {
                ChargeCard(paymentDetails, cart);
            }

            ReserveInventory(cart);

            if(notifyCustomer)
            {
                NotifyCustomer(cart);
            }
        }

        public void NotifyCustomer(Cart cart)
        {
            string customerEmail = cart.CustomerEmail;
            if (!String.IsNullOrEmpty(customerEmail))
            {
                using (var message = new MailMessage("orders@somewhere.com", customerEmail))
                using (var client = new SmtpClient("localhost"))
                {
                    message.Subject = "Your order placed on " + DateTime.Now.ToString();
                    message.Body = "Your order details: \n " + cart.ToString();

                    try
                    {
                        client.Send(message);
                    }
                    catch (Exception ex)
                    {
                        Logger.Error("Problem sending notification email", ex);
                    }
                }
            }
        }

        public void ReserveInventory(Cart cart)
        {
            foreach(var item in cart.Items)
            {
                try
                {
                    var inventorySystem = new InventorySystem();
                    inventorySystem.Reserve(item.Sku, item.Quantity);

                }
                catch (InsufficientInventoryException ex)
                {
                    Logger.Error("Insufficient inventory for item " + item.Sku, ex);
                }
                catch (Exception ex)
                {
                    Logger.Error("Problem reserving inventory", ex);
                }
            }
        }

        public void ChargeCard(PaymentDetails paymentDetails, Cart cart)
        {
            using (var paymentGateway = new PaymentGateway())
            {
                try
                {
                    paymentGateway.Credentials = "account credentials";
                    paymentGateway.CardNumber = paymentDetails.CreditCardNumber;
                    paymentGateway.ExpiresMonth = paymentDetails.ExpiresMonth;
                    paymentGateway.ExpiresYear = paymentDetails.ExpiresYear;
                    paymentGateway.NameOnCard = paymentDetails.CardholderName;
                    paymentGateway.AmountToCharge = cart.TotalAmount;

                    paymentGateway.Charge();
                }
                catch (AvsMismatchException ex)
                {
                    Logger.Error("The card gateway rejected the card based on the address provided.", ex);
                }
                catch (Exception ex)
                {
                    Logger.Error("There was a problem with your card.", ex);
                }
            }
        }
    }

As you can notice above, the Order class is part of an eCommerce Retail PoS application that supports a number of operations which include Checkout, ChargeCard, NotifyCustomer and ReserveInventory. Most of the work is carried out in the Checkout method, which first checks to see if the payment method is a Credit Card then charge the card using the payment details provided. Once this goes through, the inventory is reserved and finally if the customer has set to be notified of the transaction, the NotifyCustomer method then notify the customer that the cart was processed.

The above said methods have some dependencies; the NotifyCustomer depends on the SMTP Email, ReserveInventory uses the InventorySystem service and ChargeCard is using a payment gateway to actually charge the card.

Besides the many responsibility coupling above, one can easily see a recipe for disaster with the above in that any change to notifications, credit card processing or inventory management will affect any implementations of the Order class.

Refactored Code

To refactor the code, we need to make a judgment call where the responsibilities/behavior possibly change independently in the future, which behaviour is co-dependent on the other and will always change at the same time (“coupling”) and which behaviour will never change in the first place. In the above Order Class, we notice that the Checkout method has three main actions; processing payment, reserving inventory and notifying the customer. We can split these actions into separate interfaces IPaymentProcessor, IReservationService and INotificationService with their respective operations ProcessCreditCard, ReserveInventory and NotifyCustomerOrderCreated. The eCommerce Retail PoS application can handle different types of Orders, which can be a PoS Credit Order which can have dependencies on the refactored interfaces or the PoS Cash Order which does not have any dependency on the above as none of them are concerns of a cash transaction of the PoS:

    public abstract class Order
    {
        protected readonly Cart _cart;

        protected Order(Cart cart)
        {
            _cart = cart;
        }

        public virtual void Checkout()
        {
            // log the order in the database
        }
    }

    public class OnlineOrder : Order
    {
        private readonly INotificationService _notificationService;
        private readonly PaymentDetails _paymentDetails;
        private readonly IPaymentProcessor _paymentProcessor;
        private readonly IReservationService _reservationService;

        public OnlineOrder(Cart cart, PaymentDetails paymentDetails)
            : base(cart)
        {
            _paymentDetails = paymentDetails;
            _paymentProcessor = new PaymentProcessor();
            _reservationService = new ReservationService();
            _notificationService = new NotificationService();
        }

        public override void Checkout()
        {
            _paymentProcessor.ProcessCreditCard(_paymentDetails, _cart.TotalAmount);

            _reservationService.ReserveInventory(_cart.Items);

            _notificationService.NotifyCustomerOrderCreated(_cart);

            base.Checkout();
        }
    }

    public class PoSCreditOrder : Order
    {
        private readonly PaymentDetails _paymentDetails;
        private readonly IPaymentProcessor _paymentProcessor;

        public PoSCreditOrder(Cart cart, PaymentDetails paymentDetails)
            : base(cart)
        {
            _paymentDetails = paymentDetails;
            _paymentProcessor = new PaymentProcessor();
        }

        public override void Checkout()
        {
            _paymentProcessor.ProcessCreditCard(_paymentDetails, _cart.TotalAmount);

            base.Checkout();
        }
    }

    public class PoSCashOrder : Order
    {
        public PoSCashOrder(Cart cart)
            : base(cart)
        {
        }
    }

    #region PaymentProcessor

    public interface IPaymentProcessor
    {
        void ProcessCreditCard(PaymentDetails paymentDetails, decimal amount);
    }

    internal class PaymentProcessor : IPaymentProcessor
    {
        public void ProcessCreditCard(PaymentDetails paymentDetails, decimal amount)
        {
            throw new NotImplementedException();
        }
    }

    #endregion

    #region ReservationService

    public interface IReservationService
    {
        void ReserveInventory(IEnumerable<OrderItem> items);
    }

    public class ReservationService : IReservationService
    {
        public void ReserveInventory(IEnumerable<OrderItem> items)
        {
            throw new NotImplementedException();
        }
    }

    #endregion

    #region NotificationService

    internal interface INotificationService
    {
        void NotifyCustomerOrderCreated(Cart cart);
    }

    internal class NotificationService : INotificationService
    {
        public void NotifyCustomerOrderCreated(Cart cart)
        {
            throw new NotImplementedException();
        }
    }

    #region Cart

    public class Cart
    {
        public decimal TotalAmount { get; set; }
        
        public IEnumerable<OrderItem> Items { get; set; }

        public string CustomerEmail { get; set; }
    }

    public class OrderItem
    {
        public string Sku { get; set; }
        
        public int Quantity { get; set; }
    }

   #endregion

As Robert C Martin would point out, if two responsibilities are always expected to change at the same time then there’s no need to separate them into different classes, consequently this would result into a smell of Needless Complexity. The same is the case for responsibilities that never change – the behaviour is invariant, and there is no need to split it.