Boosting Laravel Quality with SOLID Principles: Best Practices and Examples (Part II)

Niranjan Shrestha
5 min readMar 19, 2023

--

O — Open-Closed Principle(OCP)

In previous article we understood the need of SOLID and illustrated the S — Single Responsibility Principle (SRP). Now, in this article I will illustrate the O — Open-Closed Principle (OCP).

The main idea of this principle is to avoid breaking changes when we add new feature to the existing code.

O — Open-Closed Principle (OCP)

“Software entities (classes, modules, function, etc.)”should be open for extension, but closed for modification.

I know, it may sounds weird, right? I was also very confused when I heard it for the first time. So let me show you an example.

Example 1

Let’s say we are working on a social application. It has users, posts, comments and likes. Users can like posts, so you implement this feature in the Post model. Easy, But now, users also want to like comments. You have two choices:

  1. Copy the like-related features into the Comment model
  2. You implement a generic trait that can be used in any model

Of course, we want the second option. It looks something like that:

<?php

namespace App\Traits;

trait Likable
{
public function like(): Like
{
// ...
}

public function dislike(): Void
{
// ...
}
}
<?php

namespace App\Models;

use App\Traits\Likable;

class Post extends Model
{
use Likeable;
}
<?php

namespace App\Models;

use App\Traits\Likable;

class Comment extends Model
{
use Likeable;
}

Now let’s say we need to add a chat to the app, and of course, users want to like messages. So we do this:

<?php

namespace App\Models;

use App\Traits\Likable;

class ChatMessage extends Model
{
use Likeable;
}

Here, we just added new functionality to multiple classes without changing them. We extended our classes instead of modifying them. And this is a huge win in the long term. This is why traits and polymorphism in general are amazing tools.

Example 2

Let’s say we have to calculate the total area of rectangles.

<?php

namespace App\Classes;

class Rectangle
{
public __construct(public int $width, public int $height)
{ /** */ }
}
<?php

namespace App\Classes;

class AreaCalculator
{
public function totalArea(array $rectangles)
{
$area = 0;

foreach($rectangles as $rectangle)
{
$area += $rectangle->width * $rectangle->height;
}

return $area;
}
}

now, we can use it as:

...
$areaOfRectangles = (new AreaCalculator())->totalArea([
new Rectangle(100, 200),
new Rectangle(10, 20)
]);
...

Let’s say our business requirement changes and we needs to add the area of the circle too, then we can simply create a Circle class as:

<?php

namespace App\Classes;

class Circle
{
public __construct(public int $radius)
{ /** */ }
}

and use it as:

...
$areaOfRectangles = (new AreaCalculator())->totalArea([
new Rectangle(10, 20),
new Rectangle(20, 20),
new Circle(10)
]);
...

This will not work because in AreaCalculator we need to modify the totalArea method. The totalArea method is only calculating area of rectangle not of the circle. So, to work above code we have to modify AreaCalculator which clearly violates the OCP.

To sort this issue let’s modify AreaClaculator class as:

<?php

namespace App\Classes;

class AreaCalculator
{
public function totalArea(array $shapes) // generalize the variable name
{
$area = 0;

foreach($shapes as $shape)
{
if($shape instanceOf Rectangle)
{
$area += $shape->width * $shape->height;
}
else {
$area += $shape->radius * $shape->radius * pi();
}
}

return $area;
}
}

Now, this code should work.

...
$areaOfRectangles = (new AreaCalculator())->totalArea([
new Rectangle(10, 20),
new Rectangle(20, 20),
new Circle(10)
]);
...

But, what if we need to calculate the area of triangle?

If that happens, we need to add another if case inside AreaCalculator > totalArea method, which violates OCP because this class is not closed for the modification. Every time when new shapes are added we are modifying this method.

So instead of calculating area of shape inside AreaCalculator > totalArea we will create area method inside each shape class.

<?php

namespace App\Classes;

class Rectangle
{
public __construct(public int $width, public int $height)
{ /** */ }

public function area()
{
return $this->width * $this->height;
}
}
<?php

namespace App\Classes;

class Circle
{
public __construct(public int $radius)
{ /** */ }

public function area()
{
return $this->radius * $this->radius * pi();
}
}
<?php

namespace App\Classes;

class AreaCalculator
{
public function totalArea(array $shapes) // generalize the variable name
{
$area = 0;

foreach($shapes as $shape)
{
$area += $shape->area();
}

return $area;
}
}
...
$areaOfRectangles = (new AreaCalculator())->totalArea([
new Rectangle(10, 20),
new Rectangle(20, 20),
new Circle(10)
]);
...

// this will work for every new shape
// we have to just create a new class with area method in it and it will work

OCP suggest us to use an interface we should separate the extensible behavior behind an interface.

Why an interface?

Above code is working fine, right? Let’s say we need to add a new shape of Triangle.

In Rectangle and Circle classes we have implemented area() method, But let’s say a new developer is adding this feature and he don’t know that a class should have area() method instead he/she created getArea() so it will throw an exception.

<?php

namespace App\Classes;

class Triangle
{
public __construct(public int $width, public int $height)
{ /** */ }

public function getArea()
{
return $this->width * $this->height * 1/2;
}
}

For this reason we need to have interface.

So, let’s create an interface.

<?php

namespace App\Interfaces;

interface ShapeInterface
{
public function area();
}
<?php

namespace App\Classes;

class AreaCalculator
{
public function totalArea(array $shapes)
{
$area = 0;

foreach($shapes as $shape)
{
if($shape instanceOf ShapeInterface){
$area += $shape->area();
}else{
throw new Exception(
get_class($shape) . 'should implement ShapeInterface.'
);
}
}

return $area;
}
}

Now, every shape class should implement ShapeInterface

<?php

namespace App\Classes;

use App\Interface\ShapeInterface;

class Triangle implements ShapeInterface
{
public __construct(public int $width, public int $height)
{ /** */ }

public function getArea()
{
return $this->width * $this->height * 1/2;
}
}
<?php

namespace App\Classes;

use App\Interface\ShapeInterface;

class Circle implements ShapeInterface
{
public __construct(public int $radius)
{ /** */ }

public function area()
{
return $this->radius * $this->radius * pi();
}
}
<?php

namespace App\Classes;

use App\Interface\ShapeInterface;

class Rectangle implements ShapeInterface
{
public __construct(public int $width, public int $height)
{ /** */ }

public function area()
{
return $this->width * $this->height;
}
}

<< Previous S — Single Responsibility Principle (SRP)

>> Next L — Liskov Substitution Principle(LSP)

--

--