3

Open/closed principle states that classes are closed for modifications but open for extensions. Lets say we want to design a payment system where payment can be processed by multiple processors like following:

    class Payment {
        void pay(paymentMethod) {
            switch (paymentMethod) {
               case 'PayPal':
               break;
               case 'Swift':
               break;
               default:
               break;
            }
        }
    }

    class PayPal {
        void pay() {
               //implement payment1
        }
    }

    class Swift {
        void pay() {
               //implement payment2
        }
    }

Let's say we implement both payment systems the first time. Now if for some reason any payment system implementation process is changed, should not we have to modify the relevant class? For example, if we implement PayPal and after 2-3 years PayPal's working process is changed, does not modifying the PayPal class break the open/closed principle? If it does what's the solution?

jaco0646
  • 15,303
  • 7
  • 59
  • 83
Sakibur Rahman
  • 834
  • 3
  • 10
  • 26
  • 1
    Why do you need a *payment processor* (`Payment`) and a *payment method* (for example `PayPal`)? Instead, you could simply create a `Payment` type and let `PayPal` and `Swift` be concrete implementations of that type. – Janez Kuhar Oct 18 '20 at 20:25

3 Answers3

4

Having that switch statement in your Payment classes breaks the open/closed principle because it makes the abstract idea of a Payment tightly coupled to the concrete implementations PayPal and Swift. In order to add a remove a supported payment type, you would have to edit the Payment.pay() method.

A better design uses an interface to describe what a payment provider should look like. In this case, that it must have a void pay() method.

Instead of taking a paymentMethod argument as a string, Payment.pay() should accept an instance of a class which implements the payment provider interface. It can call paymentMethod.pay() to execute the correct function. (Depending on your actual setup, it's probably better to pass this argument to the constructor than to a method).

This way it becomes trivially easy to add or remove payment providers because the Payment class does not need any knowledge whatsoever about which provider classes exist.

interface PaymentProvider {
    void pay();
}

class Payment {
    void pay(paymentMethod: PaymentProvider) {
         paymentMethod.pay();
}

class PayPal implements PaymentProvider {
    void pay() {
        //implement payment1
    }
}

class Swift implements PaymentProvider {
    void pay() {
        //implement payment2
    }
}
Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
  • 2
    Yes I totally agree with you solution. But even in your solution, lets say paypals payment implementation is changed. So in that case do not I have to modify Paypal classes payment method? Does that violate open/closed princinple? – Sakibur Rahman Oct 23 '20 at 10:36
  • 2
    If PayPal changes their implementation then some amount of code rewriting is inevitable and that’s ok. The idea behind writing flexible code is that we want to isolate those changes so that we only need to change things in one place rather than having to rework a whole system. Basically it’s ok to edit the PayPal class in response to PayPal-specific changes, but you should not have to edit the Payment class. – Linda Paiste Oct 23 '20 at 16:59
  • 4
    Here’s another way to think of it. We designed our system where we can pass in any PaymentProvider, right? So let’s say PayPal radically changes their implementation. We can write a new PayPalV2 class that implements PaymentProvider and use it as a drop-in replacement for where we used the PayPal class previously. There’s probably only one place in your code that you instantiate a new PayPay, so you just instantiate a new PayPayV2 instead. Everything else is built around expecting a PaymentProvider so that change shouldn’t matter. – Linda Paiste Oct 23 '20 at 17:06
  • Nice answer. However, what if there exists some small bug/feature upgrafe that we want to change in the existing paypals' behavior? – Sakibur Rahman Oct 24 '20 at 08:16
  • In that case I think it’s ok to edit the PayPal class. In my opinion at least, the open closed principle is more about how we design a system. I don’t think it means that it’s never allowed to modify a class. Also there is a difference between modifying the implementation of class methods and modifying their signatures (the interface). You don’t want to change the types for the arguments and the return because that would break other classes which use PayPal instances. But changing the internal behavior of a method will have no impact anywhere else. – Linda Paiste Oct 24 '20 at 18:36
2

Now if for some reason any payment system implementation process is changed, should not we have to modify the relevant class? For example, if we implement PayPal and after 2-3 years PayPal's working process is changed, does not modifying the PayPal class break the open/closed principle?

No, it doesn't break it. If the working process of PayPal changes it has to be reflected in the class that extends PayPal payment method.

I'll give you an example: Let's say that tomorrow you want to add one more payment method - Transferwise now the pattern says to us that "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification" meaning that if you need to modify any existing class in order to add new payment method you're breaking an open/closed principle and on the other side if you can just extend PaymentMethod in your new Transferwise class you are extending your system without any change and you are complying with the pattern

Arsen
  • 51
  • 2
0

I think the question here boils down to the definition of the Open/Closed Principle. Specifically, does it really mean that code should never change after it's written?

While many people (myself included) have used that definition as a substitute for the OCP, it's an oversimplification. The OCP was originally published by Bertrand Meyer in Object-Oriented Software Construction. I think the answer to this question can be found in the second edition of the book, beginning on page 60, where some "exceptions" to the OCP are noted.

  • If you have control over the original software and can rewrite it so that it will address the needs of several kinds of client at no extra complication, you should do so.
  • Neither the Open-Closed principle nor redefinition in inheritance is a way to address design flaws, let alone bugs. If there is something wrong with a module, you should fix it — not leave the original as it is and try to correct the problem in a derived module... The Open-Closed principle and associated techniques are intended for the adaptation of healthy modules: modules that, although they may not suffice for some new uses, meet their own well-defined requirements, to the satisfaction of their own clients.

Clearly, Meyer did not intend that legacy code should never be rewritten. If new requirements invalidate part of the existing logic, rewriting it may be a sensible approach and was never meant to be prohibited by the OCP.

jaco0646
  • 15,303
  • 7
  • 59
  • 83