Using discriminated unions to fix Liskov substitution principle violation

As an example of LSP violation, I will consider e-commerce payment system design. One needs to develop generic mechanism for multiple payment service providers (PSP) that are using on checkout step.

From the requirements there are different kinds of PSP in terms of integration. There are PSP that require customer to be redirected to PSP portal page, where customer needs to enter payment information, e.g. credit card. On the other hand, there are PSP that provide possibility to integrate through the API where the response is synchronous, e.g. direct payments.

Knowing that, we could start to design simple interface with two methods for different type of payments, and trying to provide several implementations.


    public interface IPaymentProvider
    {
        PaymentFormDetails GetPaymentFormDetails(Order order);
        PaymentResponse MakeDirectPayment(Order order);
    }

    public class PayPalDirectPaymentProvider : IPaymentProvider
    {
        public PaymentFormDetails GetPaymentFormDetails(Order order)
        {
            throw new NotSupportedException("Not supported for Direct payments. Used only for 3D secure payments.");
        }

        public PaymentResponse MakeDirectPayment(Order order)
        {
           //do some real job here, e.g. call real services etc
            return new PaymentResponse()
            {
                TransactionId = Guid.NewGuid().ToString()
            };
        }
    }

    public class PayPalStandartPaymentProvider : IPaymentProvider
    {
        public PaymentFormDetails GetPaymentFormDetails(Order order)
        {
            return new PaymentFormDetails();
        }

        public PaymentResponse MakeDirectPayment(Order order)
        {
            throw new NotSupportedException("Direct payments are not supported by PayPal direct.");
        }
    }

    public class PaymentResponse
    {
        public string TransactionId { get; set; }
    }

    public class Order
    {
        public decimal Amount { get; set; } 
        public string OrderNumber { get; set; }
    }

    public class PaymentFormDetails
    {
        public string PaymentGatewayUrl { get; set; }
        public string OrderNumber { get; set; }
        public decimal Amount { get; set; }
    }

Unfortunately there is no way for specific payment provider to implement both of methods, so we have to throw NotSupportedException. That is clearly indicates LSP violation.

As we try to adhere to SOLID principles, let’s try to fix the violation.

LSP violation fix#1 – NotSupportedException

First attempt could be to introduce marker interface, it could greatly simplified interface and implementations themselves,
But now calling side should perform downcasting from interface to its implementation in order to correctly interpret the result.

 public interface IPaymentProvider
    {
        IPaymentRequestProcessingResult ProcessPaymentRequest(Order order);
    }

    public interface IPaymentRequestProcessingResult
    {
    }

    public class PaymentResponse : IPaymentRequestProcessingResult
    {
        public string TransactionId { get; set; }
    }

    public class PaymentFormDetails : IPaymentRequestProcessingResult
    {
        public string PaymentGatewayUrl { get; set; }
        public string OrderNumber { get; set; }
        public decimal Amount { get; set; }
    }

    public class PayPalDirectPaymentProvider : IPaymentProvider
    {
        public IPaymentRequestProcessingResult ProcessPaymentRequest(Order order)
        {
            //do some real job here, e.g. call real services etc
            return new PaymentResponse()
            {
                TransactionId = Guid.NewGuid().ToString()
            };
        }
    }

    public class PayPalStandartPaymentProvider : IPaymentProvider
    {
        public IPaymentRequestProcessingResult ProcessPaymentRequest(Order order)
        {
            return new PaymentFormDetails()
            {
                Amount = order.Amount,
                OrderNumber = order.OrderNumber,
                PaymentGatewayUrl = "https://www.paypal.com"
            };
        }
    }

Remove LSP violation#2 – Downcasting and type checking. Introduce discriminated union

Discriminated union is terms came from functional langauges, particularly from F#. Base on Microsoft documentation

Discriminated unions provide support for values that can be one of a number of named cases, possibly each with different values and types. Discriminated unions are useful for heterogeneous data;

https://docs.microsoft.com/en-us/dotnet/articles/fsharp/language-reference/discriminated-unions

Discriminated unions and not supported out of the box in C#, but there are several ready to use implementations over the Internet. I used implementation from here http://pastebin.com/EEdvVh2R

public sealed class Union2<A, B>
    {
        readonly A Item1;
        readonly B Item2;
        int tag;

        public Union2(A item) { Item1 = item; tag = 0; }
        public Union2(B item) { Item2 = item; tag = 1; }

        public T Match<T>(Func<A, T> f, Func<B, T> g)
        {
            switch (tag)
            {
                case 0: return f(Item1);
                case 1: return g(Item2);
                default: throw new Exception("Unrecognized tag value: " + tag);
            }
        }
    }

Than we will remove from

  public interface IPaymentProvider
    {
        PaymentRequestProcessingResult ProcessPaymentRequest(Order order);
    }

    public class PaymentRequestProcessingResult: Union2<PaymentResponse, PaymentFormDetails>
    {
        public PaymentRequestProcessingResult(PaymentResponse item) : base(item)
        {
        }

        public PaymentRequestProcessingResult(PaymentFormDetails item) : base(item)
        {
        }
    }

And implementations now:

 public class PayPalDirectPaymentProvider : IPaymentProvider
    {
        public PaymentRequestProcessingResult ProcessPaymentRequest(Order order)
        {
            //do some real job here, e.g. call real services etc
            return new PaymentRequestProcessingResult(
                new PaymentResponse()
                {
                    TransactionId = Guid.NewGuid().ToString()
                });
        }
    }

    public class PayPalStandartPaymentProvider : IPaymentProvider
    {
        public PaymentRequestProcessingResult ProcessPaymentRequest(Order order)
        {
            return new PaymentRequestProcessingResult(new PaymentFormDetails()
            {
                Amount = order.Amount,
                OrderNumber = order.OrderNumber,
                PaymentGatewayUrl = "https://www.paypal.com"
            });
        }
    }

And finally usage,

public static void Main()
        {
            var order = new Order
            {
                Amount = 20m,
                OrderNumber = "00001"
            };

            Console.WriteLine("Select payment provider (1 - PayPal Direct, 2 - PayPal standart):");
            var pl = int.Parse(Console.ReadLine());
            var paymentProvider = GetPaymentProvider(pl);

            var res = paymentProvider.ProcessPaymentRequest(order);

            Console.WriteLine(res.Match(
                r => "Direct payment succeed. Transaction ID:" + r.TransactionId,
                fd => "Standart payment providers. Redirecting to PSP. Paymew gateway url:" + fd.PaymentGatewayUrl));

            Console.Read();
        }

ProcessPaymentRequest method results are clearly defined and allow to use them in type-safe manner.

Review

Let’s review what we achieved so far:

  • Removed throwing NotSupportedException
  • Eliminate downcasting to specific class types
  • Enable type-safe way of working with possible payment processing results
This entry was posted in object-oriented-principles, software-patterns. Bookmark the permalink.

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 )

Google+ photo

You are commenting using your Google+ 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 )

w

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.