DEV Community

DaleLanto
DaleLanto

Posted on • Edited on

SOLID Principles -Object Oriented Programming in PHP ⭐⭐⭐⭐⭐

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

These principles establish practices that lend to developing software with considerations for maintaining and extending as the project grows. Adopting these practices can also contribute to avoiding code smells, refactoring code, and Agile or Adaptive software development.

SOLID stands for:

S - Single-responsiblity Principle
O - Open-closed Principle
L - Liskov Substitution Principle
I - Interface Segregation Principle
D - Dependency Inversion Principle

Now let's talk about the first one:
Single Responsibility Principle

"A Class Should Have One, And Only One Responsibility"

It means that if our class assumes more than one responsibility we will have a high coupling. The cause is that our code will be fragile at any changes.

Suppose we have a User class like the following:

<?php

class User {

    private $email;

    // Getter and setter...

    public function store() {
        // Store attributes into a database...
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, the method store is out of the scope and this responsibility should belong to a class that manages the database.

The solution here is to create two classes each with proper responsibilities.

<?php

class User {

    private $email;

    // Getter and setter...
}

class UserDB {

    public function store(User $user) {
        // Store the user into a database...
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let's move on to the O in SOLID
Open-closed Principle

Objects or entities should be open for extension but closed for modification.

According to this principle, a software entity must be easily extensible with new features without having to modify its existing code in use.

Suppose we have to calculate the total area of some objects and to do that we need an AreaCalculator class that does only a sum of each shape area.

The issue here is that each shape has a different method to calculate its own area.

<?php

class Rectangle {

    public $width;
    public $height;

    public function __construct($width, $height) {
        $this->width = $width;
        $this->height = $height;
    }
}

class Square {

    public $length;

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


class AreaCalculator {

    protected $shapes;

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

    public function sum() {
        $area = [];

        foreach($this->shapes as $shape) {
            if($shape instanceof Square) {
                $area[] = pow($shape->length, 2);
            } else if($shape instanceof Rectangle) {
                $area[] = $shape->width * $shape->height;
            }
        }

        return array_sum($area);
    }
}
Enter fullscreen mode Exit fullscreen mode

If we add another shape like a Circle we have to change the AreaCalculator in order to calculate the new shape area and this is not sustainable.

The solution here is to create a simple Shape interface that has the area method and will be implemented by all other shapes.

<?php

interface Shape {
    public function area();
}

class Rectangle implements Shape {

    private $width;
    private $height;

    public function __construct($width, $height) {
        $this->width = $width;
        $this->height = $height;
    }

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

class Square implements Shape {

    private $length;

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

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


class AreaCalculator {

    protected $shapes;

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

    public function sum() {
        $area = [];

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

        return array_sum($area);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this way, we will use only one method to calculate the sum and if we need to add a new shape it will just implement the Shape interface.

The third principle in SOLID L stands for:
Liskov Substition Princple

Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

The principle says that objects must be replaceable by instances of their subtypes without altering the correct functioning of our system.

I know this is hard to understand so I separated the meaning in 5 parts.

1. Child function arguments must match function arguments of parent

2. Child function return type must match parent function return type

3. Child pre-conditions cannot be greater than parent function pre-conditions

4. Child function post-conditions cannot be lesser than parent function post-conditions.

5. Exceptions thrown by child method must be the same as or inherit from an exception thrown by the parent method.
Enter fullscreen mode Exit fullscreen mode

To fully understand this here's a scenario:

Imagine managing two types of coffee machine. According to the user plan, we will use a basic or a premium coffee machine, the only difference is that the premium machine makes a good vanilla coffee plus than the basic machine.

<?php

interface CoffeeMachineInterface {
    public function brewCoffee($selection);
}


class BasicCoffeeMachine implements CoffeeMachineInterface {

    public function brewCoffee($selection) {
        switch ($selection) {
            case 'ESPRESSO':
                return $this->brewEspresso();
            default:
                throw new CoffeeException('Selection not supported');
        }
    }

    protected function brewEspresso() {
        // Brew an espresso...
    }
}


class PremiumCoffeeMachine extends BasicCoffeeMachine {

    public function brewCoffee($selection) {
        switch ($selection) {
            case 'ESPRESSO':
                return $this->brewEspresso();
            case 'VANILLA':
                return $this->brewVanillaCoffee();
            default:
                throw new CoffeeException('Selection not supported');
        }
    }

    protected function brewVanillaCoffee() {
        // Brew a vanilla coffee...
    }
}


function getCoffeeMachine(User $user) {
    switch ($user->getPlan()) {
        case 'PREMIUM':
            return new PremiumCoffeeMachine();
        case 'BASIC':
        default:
            return new BasicCoffeeMachine();
    }
}


function prepareCoffee(User $user, $selection) {
    $coffeeMachine = getCoffeeMachine($user);
    return $coffeeMachine->brewCoffee($selection);
}
Enter fullscreen mode Exit fullscreen mode

The main program behavior must be the same for both machines.

The fourth principle I stands for:
Interface Segregation Principle

A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.

This principle defines that a class should never implement an interface that does not go to use.

In that case, means that in our implementations we will have methods that don’t need.

The solution is to develop specific interfaces instead of general-purpose interfaces.

Here's a scenario, imagine we invent the FutureCar that can both fly and drive…

<?php

interface VehicleInterface {
    public function drive();
    public function fly();
}

class FutureCar implements VehicleInterface {

    public function drive() {
        echo 'Driving a future car!';
    }

    public function fly() {
        echo 'Flying a future car!';
    }
}

class Car implements VehicleInterface {

    public function drive() {
        echo 'Driving a car!';
    }

    public function fly() {
        throw new Exception('Not implemented method');
    }
}

class Airplane implements VehicleInterface {

    public function drive() {
        throw new Exception('Not implemented method');
    }

    public function fly() {
        echo 'Flying an airplane!';
    }
}
Enter fullscreen mode Exit fullscreen mode

The main issue, as you can see, is that the Car and Airplane have methods that don’t use.

The solution is to split the VehicleInterface into two more specific interfaces that are used only when it’s necessary, like the following:

<?php

interface CarInterface {
    public function drive();
}

interface AirplaneInterface {
    public function fly();
}

class FutureCar implements CarInterface, AirplaneInterface {

    public function drive() {
        echo 'Driving a future car!';
    }

    public function fly() {
        echo 'Flying a future car!';
    }
}

class Car implements CarInterface {

    public function drive() {
        echo 'Driving a car!';
    }
}

class Airplane implements AirplaneInterface {

    public function fly() {
        echo 'Flying an airplane!';
    }
}
Enter fullscreen mode Exit fullscreen mode

Last but not the least is D which stands for:
Dependency Inversion Principle

Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions.

This principle means that a particular class should not depend directly on another class but on an abstraction of this class.

This principle allows for decoupling and more code reusability.

Let’s get the first example of the UserDB class. This class could depend on a DB connection:

<?php

class UserDB {

    private $dbConnection;

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

    public function store(User $user) {
        // Store the user into a database...
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, the UserDB class depends directly from the MySQL database.

That means that if we would change the database engine in use we need to rewrite this class and violate the Open-Close Principle.

The solution is to develop an abstraction of database connection:

<?php

interface DBConnectionInterface {
    public function connect();
}

class MySQLConnection implements DBConnectionInterface {

    public function connect() {
        // Return the MySQL connection...
    }
}

class UserDB {

    private $dbConnection;

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

    public function store(User $user) {
        // Store the user into a database...
    }
}
Enter fullscreen mode Exit fullscreen mode

This code establishes that both the high-level and low-level modules depend on abstraction.

Hurray! We are done with the 5 SOLID Principle of Object Oriented Programming! You can use these principles represent the state of the art of the code quality and following them permit you to write software that will be easily extended, reusable and refactored.

Projects that adhere to SOLID principles can be shared with collaborators, extended, modified, tested, and refactored with fewer complications.

Top comments (0)