Let's start from the beginning and remind ourselves what was SOLID invented for. First of all, it is worth noting that SOLID was invented to structure object-oriented code, to describe the design of objects and architecture in general. Creating classes and wrapping functions in methods is not enough, this is fundamentally the wrong approach. Unfortunately, many developers still do not understand or simply do not know how to write correctly and use the power of object-oriented programming. I still think that opponents of object-oriented code are just people who don’t understand its basics or don’t understand how to use it correctly. So let's go through the basics again, step by step.

S.O.L.I.D stands for:

SOLID in very short and simple words

Using these design principals will help you to

  • write code that's easy to maintain;
  • easily extend the software with new functionality without breaking existing ones;
  • spend less time reading and more time writing code.

Single-responsibility principle (SRP for short)

A class should have only one reason to change

Single-responsibility Principle

class CreateOrderService
{
    private RemoteService $remoteService;

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

    public function execute(CreateOrderRequest $request): OrderId
    {
        try {
            $response = $this->remoteService->create($request);

            return new OrderId($response->orderId);
        } catch (Exception $e) {
            throw new OrderCreationFailed($e);
        }
    }
}

But be careful, this principle is not as simple as it seems.

Things that are similar to change should be stored in one place, or everything that changes together must be kept in one place.

That is, if we change some operation, then we must change it in one place. SRP not only requires splitting while it is possible to split, but also not overdoing it: do not split linked things. Do not complicate unnecessarily!

The scope of SRP is not limited to OOP and SOLID. It applies to methods, functions, classes, modules, microservices and services. If you think about it, this is almost the fundamental principle of all engineering.

Open-closed principle

A class should be open for extension, but closed for modification.

Open-closed principle

interface AreaCalculatable
{
    public function area(): float;
}

class AreaCalculator
{
    public function calculate(AreaCalculatable ...$shapes): float
    {
        return array_reduce(
            $shapes,
            function($carry, AreaCalculatable $shape) {
                return $carry + $shape->area();
            },
            0.0
        );
    }
}

As you can see from the example above, we do not modify or manipulate shapes to calculate the area, we just extend shape objects to calculate that for us.

Liskov substitution principle

Don't switch implementations, unless they give the same result.

One of the most poorly-known principle, maybe because its original explanation has a lot of math, but it is quite simple:

  • The overridden method shouldn't remain empty.
  • The overridden method shouldn't throw an error.
  • Base class or interface behaviour should not go for modification (rework) as because of derived class behaviours.

Liskov substitution principle

Imagine you had setWidth and setHeight methods on your Rectangle base class; this seems perfectly logical. However, if your Rectangle reference pointed to a Square, then setWidth and setHeight doesn't make sense because setting one would change the other to match it. In this case, Square fails the Liskov Substitution Test with Rectangle and the abstraction of having Square inherit from Rectangle is a bad one.

class Rectangle
{
    protected int $height;
    protected int $width;

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

    public function setHeight(int $height): self
    {
        $instance = clone $this;
        $instance->height = $height;

        return $instance;
    }

    public function setWidth(int $width): self
    {
        $instance = clone $this;
        $instance->width = $width;

        return $instance;
    }

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

class Square extends Rectangle
{
    public function __construct(int $side)
    {
        $this->height = $side;
        $this->width = $side;
    }
}

$square = new Square(5);
echo $square->area(); // 25

$square = $square->setHeight(4);
echo $square->area(); // 20, but expected 16

Liskov substitution principle duck example

Interface segregation principle

Do not depend on methods you don't use.

Interface segregation principle

// WRONG
interface Worker
{
    public function startWorking(): void;

    public function stopWorking(): void;

    public function makeLunch(): void;

    public function visitRestroom(): void;
}

class Employee implements Worker
{}

class Android implements Worker
{
    // Wrong, as unnecessary methods makeLunch() and visitRestroom() will be empty
}

The main idea behind this principle, you have to split interfaces unless they become reliable for all classes, that implement this interface. Classes should not contain empty implementations, namely methods:

// CORRECT
interface Worker
{
    public function startWorking(): void;

    public function stopWorking(): void;
}

interface HumanWorker
{
    public function makeLunch(): void;

    public function visitRestroom(): void;
}

class Employee implements Worker, HumanWorker
{}

class Android implements Worker
{}

Dependency Inversion principle

Depend on abstractions, not concretions.

Dependency Inversion Principle

// WRONG
class UserService
{
    private MysqlUserRepository $userRepository;

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

    public function create(User $user): void
    {
        $this->userRepository
            ->execute(sprintf(
                'INSERT INTO users (id, name) VALUES (%s, %s)',
                $user->id,
                $user->name
            ));
    }
}

Your application should not depend on any concretions, code should be written without any specific knowledge about third-party services, so we can any time replace it with the implementation that follows our requirements (interface).

// CORRECT
interface UserRepository
{
    public function create(User $user): void;
}

class MysqlUserRepository implements UserRepository
{
    private PDOStatement $statement;

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

    public function create(User $user): void
    {
        $this->statement
            ->execute(sprintf(
                'INSERT INTO users (id, name) VALUES (%s, %s)',
                $user->id,
                $user->name
            ));
    }
}

class UserService
{
    private UserRepository $userRepository;

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

    public function create(User $user): void
    {
        $this->userRepository->create($user);
    }
}

Now, there is absolutely no pain to write another implementation of UserRepository and replace dependency in the UserService.


Wrap it all together

Let's assume how can we improve our application to be SOLID-compliant and improve team performance by using SOLID design principles.

  1. I would recommend starting from Segregate the interfaces.

  2. Do not depend on concrete implementations, use dependency injection with bound interfaces.

  3. Be mindful about what your program expects. Do not access array keys directly, work with objects with the defined structure only.

  4. Separate the responsibilities, do not mix all executions in one place.

  5. Close the code for modification and depend only on interfaces while injecting dependencies.

As a result, your team will get a performance boost as they are:

  • spending less time reading code;
  • reducing the time needed to extend the software;
  • reducing bugs by closing the core of the system;
  • improving productivity and reducing costs.

TL;DR: Conclusion

SOLID is a collection of great principles that can improve your application structure and team performance, but keep in mind: Software Design Principles are your tools, not your goals!

Do not overuse these principles, especially when you need a very simple solution. Always keep in mind, that your code should be easy to read and open for extension, but not for modification. Otherwise, someday you will need to refactor away from SRP.