This article is part 2 of Becoming a better Laravel developer by using the SOLID design principles article series. You can see part one S - Single-responsibility principle here
Today, we will discuss the second letter of SOLID which is O - Open-closed principle.
What is O - Open-closed principle
According to the official definition
software entities should be open for extension, but closed for modification.
Entities can be classes, modules, functions, etc. The definition meaning that extends functionality by adding new code instead of changing existing code.
It is quite confusing to understand by title. So let's see it with examples in Laravel.
Let's say we have an e-commerce system that involves payments. So, we have PaymentController
and in that controller, we have pay
method which has two payment methods for now 1) Credit Card 2) Paypal and looks like below:
PaymentController.php
public function pay(Request $request)
{
$payment = new Payment();
if($type == 'credit'){
// do credit card payments
$payment->paymentWithCreditCard();
}else {
//do paypal payments
$payment->paymentWithPaypal();
}
}
and our Payment
class looks like this:
Payment.php
class Payment
{
public function paymentWithCreditCard()
{
// logic for credit card payments
}
public function paymentWithPaypal()
{
// logic for paypal payments
}
}
What happens when we want to add a new payment method let's say wire transfer
In pay
method we add a new if condition and call an appropriate method from the Payment
class.
So the pay
method looks like below:
PaymentController.php
public function pay(Request $request)
{
$payment = new Payment();
if($type == 'credit'){
// do credit card payments
$payment->paymentWithCreditCard();
}else if($type == 'paypal') {
//do paypal payments
$payment->paymentWithPaypal();
}
else {
//do wire transfer payments
$payment->paymentWithWiretransfer();
}
}
and modifying the Payment
class:
Payment.php
class Payment
{
public function paymentWithCreditCard()
{
// logic for credit card payments
}
public function paymentWithPaypal()
{
// logic for paypal payments
}
public function paymentWithWiretransfer()
{
// logic for wire transfer payments
}
}
So what will happen next What if we have another payment method called Cash on Delivery, we have added another if
and then a new method in the Payment
class. Another payment method comes called Coupon Payment and we are again modifying classes. Remember what does this principle says, Open for extension but close for modifications. so we are breaking this principle here.
So, how to fix it, let's apply Open Close Principle here.
Let's make a new interface
with a method named pay
read more about interfaces in PHP.
PayableInterface.php
interface PayableInterface
{
public function pay();
}
Make a class
for each payment method and extend it with the PayableInterface
interface.
CreditCardPayment.php
class CreditCardPayment implements PayableInterface
{
public function pay()
{
// Implement Credit Card payment logic
}
}
PaypalPayment.php
class PaypalPayment implements PayableInterface
{
public function pay()
{
// Implement paypall payment logic
}
}
CreditCardPayment.php
class WiretransferPayment implements PayableInterface
{
public function pay()
{
// Implement Wire transfer payment logic
}
}
Now, make a new class
PaymentFactory.php
class PaymentFactory
{
public function initializePayment($type)
{
if($type == 'credit'){
// do credit card payments
return new CreditCardPayment;
}else if($type == 'paypal') {
//do paypal payments
return new PaypalPayment;
}
else if ($type == 'wire') {
//do wire transfer payments
return new WiretransferPayment;
}
}
}
and finally our pay
method in
PaymentController.php
. This pay method asks PaymentFactory
todo the payments and factory handle itself everything.
public function pay(Request $request)
{
$paymentFactory = new PaymentFactory();
$paymentMethod = $paymentFactory->initializePayment($request->type);
$paymentMethod->pay();
}
So what will happen next What if we have another payment method called Cash on Delivery we will make a new class
called CashOnDeliveryPayment.php
and implement the same payableInterface
which will give us pay
method to put cash on delivery logic there.
That's it. We solve the issue of modifying the classes now if the new payment method comes we extend functionality by adding new class instead of modifying the existing one.
Please comment down below if you do not understand any part of the article.
Top comments (8)
In the PaymentFactory class, we are using the same if else statements to manage different payments. Don't you think we are breaking the rule only with implementing the logic with a different wayt?
Good question please see above
comment by @connor leech
Please give credit to the original author of your example Katerina Trajchevska - the same example comes from this video: youtube.com/watch?v=rtmFCcjEgEw
Excelente. If we have to add another payment method, we have to add another “else if” in initializePayment?
No. As i explained in article please check, you don't need to change controller now. You need to add an other payment class and implement payableInterface. Then in payment class add if else type check and return appropriate class. Done
Instead of using if-else there could be an array on the PaymentFactory class where the type maps to the class, like 'credit' => 'CreditCardPayment' and then instantiate the correct class based on the type. Definitely an opportunity for improvement by using constants here. That if-else is not so bueno imho. Thanks for the article! Looking forward to the rest :)
Yes we can use this approach. As my examples are not perfect. There is a plenty of room for improvements.
@Abrar ahmad ,,
You can achieve OCP by using trait also..
What you think about this.