Some of the content and diagrams in this article have been quoted from the Uncle Bob’s speech in the link below.
I highly recommend you to watch the whole video for better understanding.
Understanding the Open-Closed Principle (OCP)
The Open-Closed Principle is the ‘O’ in the SOLID acronym. This principle states that software entities, such as classes, modules, or functions, should be open for extension but closed for modification. In simpler terms, you should be able to add new functionality without altering existing code.
The idea was introduced by Bertrand Meyer and here is what exactly he said:
Modules should be open for extension, but closed for modification.
Let’s have a look at a Dart example violating the OCP and try to understand what is wrong with it.
class CashPayment {
void pay() {
print('Pay with cash');
}
}
class CreditCardPayment {
void pay() {
print('Pay with credit card');
}
}
class PaymentService {
void pay(dynamic paymentMethod) {
if (paymentMethod is CashPayment) {
paymentMethod.pay();
} else if (paymentMethod is CreditCardPayment) {
paymentMethod.pay();
} else {
throw Exception('Unknown payment method');
}
}
}
void main() {
final paymentService = PaymentService();
paymentService.pay(CashPayment());
}
In the example above, imagine that you would like to add a new feature such as PayPal Payment. Which changes would you make for it? Would you modify your code to add this new feature, and how?
In order to add PayPal payment, we could simply create a PayPalPayment class just like the CashPayment or CreditCartPayment classes. But it is not the end. As you can see the PaymentService is invoked to perform a payment which means that we have to modify the if-else statement and add a new condition to handle the PayPayPayment.
So it is not that difficult, right? It it easy to change BUT we are considering such a simple example. Imagine that we have a large-scale code base and many other modules use the PaymentService as a dependency with if-else and switch statements inside. Then, for each new feature going to be added, we would have to modify those statements to add the new payment method. In such a case our code base would be fragile. It would be so easy to break our code while making changes.
As we can see, the example above is violating the OCP since we have to modify the existing code to extend it.
Now, let’s check this version:
abstract class IPaymentMethod {
void pay();
}
class CashPayment implements IPaymentMethod {
@override
void pay() {
print('Pay with cash');
}
}
class CreditCardPayment implements IPaymentMethod {
@override
void pay() {
print('Pay with credit card');
}
}
class PayPalPayment implements IPaymentMethod {
@override
void pay() {
print('Pay with PayPal');
}
}
class PaymentService {
void pay(IPaymentMethod paymentMethod) {
paymentMethod.pay();
}
}
void main() {
final paymentService = PaymentService();
paymentService.pay(PayPalPayment());
}
In the new example, the OCP is properly followed. We introduced an interface IPaymentMethod that defines the pay() method. Each payment method implements this interface, ensuring a consistent way to perform the pay method.
The PaymentService class now accepts any object that implements the IPaymentMethod interface. This makes the software open for extensions, as new payment methods can be added without modifying existing code. The PaymentService class remains closed for modification, adhering to the OCP.
As a conclusion, it is possible to extend the payment methods only by creating a new class inheriting the IPaymentMethod and by playing the main method a little bit. PaymentService does not even has to know about the specific payment methods.
Using Open-Closed Principle in this way would help us maintain our codebase in an easy way.