SOLID Principles in OOP: Mastering Object-Oriented Design

Projects that adhere to SOLID principles can be more easily shared with other developers, extended, modified, tested and refactored. We will explain these principles and show you examples of their application.

SOLID is an acronym for the first five principles of object-oriented design (OOD) by Robert C. Martin (also known as Uncle Bob):

  • S – Single Responsibility Principle
  • O – Open-Closed Principle
  • L – Liskov Substitution Principle
  • I – Interface Segregation Principle
  • D – Dependency Inversion Principle

These principles establish practices that help develop software with maintainability and extensibility in mind as the project grows. Applying these principles can also help avoid code smells and support the process of code refactoring, and they are relevant to agile or adaptive software development.

Note: We use PHP in our sample code – but the SOLID principles can apply to different programming languages.

Single-Responsibility Principle

The Single-Responsibility Principle (SRP) states: a class should have only one reason to change, which means that a class should have only one task.

For example, we can consider an application that receives a collection of shapes – circles and squares – and calculates the sum of the areas of all the shapes in the collection.

To implement this example, we first create the classes for the shapes and set the required parameters in the constructors.

For squares, we need the side length:

class Square
{
    public $length;

    public function __construct($length)
    {
        $this->length = $length;
    }
}

For circles we need the radius:


class Circle
{
    public $radius;

    public function __construct($radius)
    {
        $this->radius = $radius;
    }
}


Next, we create the AreaCalculator class and write the logic to sum up the areas of all the shapes provided. The area of a square is calculated by taking the side length squared. The area of a circle is multiplied by π and then taken squared.


class AreaCalculator
{
    protected $shapes;

    public function __construct($shapes = [])
    {
        $this->shapes = $shapes;
    }

    public function sum()
    {
        foreach ($this->shapes as $shape) {
            if (is_a($shape, 'Square')) {
                $area[] = pow($shape->length, 2);
            } elseif (is_a($shape, 'Circle')) {
                $area[] = pi() * pow($shape->radius, 2);
            }
        }

        return array_sum($area);
    }

    public function output()
    {
        return implode('', [
            'Sum of the areas of the provided shapes: ',
            $this->sum(),
        ]);
    }
}


To use the AreaCalculator class, you must instantiate the class and pass an array of shapes. The result is displayed at the bottom of the page. Here is an example with a collection of three shapes:

  • A circle with a radius of 2
  • A square with a side length of 5
  • Another square with a side length of 6

$shapes = [
    new Circle(2),
    new Square(5),
    new Square(6),
];

$areas = new AreaCalculator($shapes);

echo $areas->output();


The problem with the output method is that the AreaCalculator handles the logic to output the data. Imagine you want to convert the output to another format like JSON. All the logic would be handled by the AreaCalculator class, which would violate the Single-Responsibility Principle. The AreaCalculator class should only care about the sum of the areas of the provided shapes and not care whether the user wants JSON or HTML.

To solve this problem, you can create a separate SumCalculatorOutputter class and use this new class to handle the logic for outputting the data:


class SumCalculatorOutputter
{
    protected $calculator;

    public function __construct(AreaCalculator $calculator)
    {
        $this->calculator = $calculator;
    }

    public function JSON()
    {
        $data = [
            'sum' => $this->calculator->sum(),
        ];

        return json_encode($data);
    }

    public function HTML()
    {
        return implode('', [
            'Sum of the areas of the provided shapes: ',
            $this->calculator->sum(),
        ]);
    }
}


The SumCalculatorOutputter class would work as follows:

$shapes = [
    new Circle(2),
    new Square(5),
    new Square(6),
];

$areas = new AreaCalculator($shapes);
$output = new SumCalculatorOutputter($areas);

echo $output->JSON();
echo $output->HTML();


Now the logic needed to output the data to the user is handled by the SumCalculatorOutputter class. This satisfies the single-responsibility principle.

Open-Closed Principle

The Open-Closed Principle (OCP) states: Objects or entities should be open for extensions but closed for modifications. That is, a class should be extensible without modifying the class itself.

Understanding the AreaCalculator class

Let’s look again at the AreaCalculator class and focus on the sum method:

class AreaCalculator
{
    protected $shapes;

    public function __construct($shapes = [])
    {
        $this->shapes = $shapes;
    }

    public function sum()
    {
        foreach ($this->shapes as $shape) {
            if (is_a($shape, 'Square')) {
                $area[] = pow($shape->length, 2);
            } elseif (is_a($shape, 'Circle')) {
                $area[] = pi() * pow($shape->radius, 2);
            }
        }

        return array_sum($area);
    }
}


Suppose the user wants to calculate the sum of other shapes like triangles, pentagons, hexagons, etc. Then you would have to constantly change this code and add more if/else blocks. This would violate the open-closed principle.

Improving the sum method

A better way to design the sum method is to remove the logic for calculating the area of each shape from the method of the AreaCalculator class and append it to the classes of the shapes. Here is the area method defined in the Square class:

class Square
{
    public $length;

    public function __construct($length)
    {
        $this->length = $length;
    }

    public function area()
    {
        return pow($this->length, 2);
    }
}


And here is the area method defined in the Circle class:

class Circle
{
    public $radius;

    public function __construct($radius)
    {
        $this->radius = $radius;
    }

    public function area()
    {
        return pi() * pow($shape->radius, 2);
    }
}


The sum method for AreaCalculator can then be rewritten as follows:

class AreaCalculator
{
    // ...

    public function sum()
    {
        foreach ($this->shapes as $shape) {
            $area[] = $shape->area();
        }

        return array_sum($area);
    }
}


Now you can create another form class and add it when calculating the sum without any code changes.

Ensuring Shape Integrity

However, another problem arises. How do you know that the object passed to the AreaCalculator is actually a shape, or if the shape has a method called area? Using an interface is an essential part of SOLID.

Create a ShapeInterface that supports area:

interface ShapeInterface
{
    public function area();
}


Change your shape classes to implement the ShapeInterface. Here is the update for Square:

class Square implements ShapeInterface
{
    // ...
}


And here is the update for Circle:

class Circle implements ShapeInterface
{
    // ...
}


In the sum method for AreaCalculator you can check if the provided shapes are actually instances of ShapeInterface. Otherwise, throw an exception:

class AreaCalculator
{
    // ...

    public function sum()
    {
        foreach ($this->shapes as $shape) {
            if (is_a($shape, 'ShapeInterface')) {
                $area[] = $shape->area();
                continue;
            }

            throw new AreaCalculatorInvalidShapeException();
        }

        return array_sum($area);
    }
}


This fulfills the Open-Closed Principle.

Liskov Substitution Principle

The Liskov Substitution Principle (LSP) ensures that any subclass can serve as a substitute for its base class without affecting the correctness of a program.

AreaCalculator and VolumeCalculator Example

Building on the AreaCalculator example, consider a new class VolumeCalculator:

class VolumeCalculator extends AreaCalculator
{
    public function sum()
    {
        // Logic to calculate volumes, returning a numerical value
        return $summedData;
    }
}

When the VolumeCalculator adheres to LSP, it can be used anywhere an AreaCalculator is expected without errors.

Interface Segregation Principle

The Interface Segregation Principle (ISP) ensures that a class should not be forced to implement interfaces it does not use.

Separation of Shape Interfaces

interface ShapeInterface
{
    public function area();
}

interface ThreeDimensionalShapeInterface
{
    public function volume();
}

Using separate interfaces for different responsibilities prevents classes from implementing unnecessary methods, satisfying ISP.

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules, but both should depend on abstractions.

PasswordReminder and Database Connection Example


interface DBConnectionInterface
{
    public function connect();
}

class MySQLConnection implements DBConnectionInterface
{
    public function connect()
    {
        // Database connection logic
    }
}

class PasswordReminder
{
    private $dbConnection;

    public function __construct(DBConnectionInterface $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}


By depending on a SOLID interface, the PasswordReminder class remains decoupled from database connection details, adhering to DIP. OOP: Mastering Object-Oriented Design

Create a Free Account

Register now and get access to our Cloud Services.

Posts you might be interested in: