How to become a better developer with SOLID principles

Sven Hanisch, 2 February 2023

SOLID is a popular set of design principles that are used in object-oriented software development. It was introduced by Robert C. Martin (aka. Uncle Bob) in his papers “Design Principles and Design Patterns“.

Using these principles should encourage the developer to create code that is more maintainable, reusable and understandable.

It will help to develop more flexible software with CLEAN CODE.

Source: https://www.wlion.com/blog/5-reasons-you-should-be-writing-clean-code/

1. What is it all about?

Everything is about code quality. But how could this be measured? There is no hard value.

For example, there is a reusability of 76,9%. Even if you could measure it, would you say this is good or bad? Is it enough?

Source: licensed 2008 Focus Shift

As shown in the picture, the best way to measure the code quality is WTFs/minute.
WTFs/minute measures the number of “works that frustrate” that a developer can read per minute, also known as code quality.

The goal must be to get from the right door to the left door by reducing the WTFs per minute. That’s hard work and sometimes not easy at all! But spoiler… SOLID can help here ;-)

There are some problems that are maybe all well-known when developing software:

Applications are growing and the complexity is increasing.
The goal is always to design something that is lasting, can be reused as often as possible, with minimal change needed in the future.
But how larger the application is growing, the more complex the construct of this application and the architecture will be.

Understandability and readability of the code gets more difficult.
For other developers or even for the developer himself. For example when stopping the development for this project for a while and coming back after a few months or even when on vacation. Then sometimes (always) it’s hard to know what is going on in these codelines.

Dependencies are growing.
The more dependencides the code will have, the more complicated it will be to make changes on the code without affecting other parts of the project. This also reduces reusability of the code.
For example having 10 dependencies in one class AND then changing an implemented class…. Affects maybe part of the base class.

Classes are getting bigger.
They become a place for everything. It doesn’t matter if it does really fit to the class. For example, you name the class with the postfix “Handler” …so it can handle everything, and it’s responsible for everything.

And as a result for all of this: Code quality is decreasing!

2. SOLID principles - Please help us!

So, what is SOLID and how does it help to write better code?
SOLID are basically 5 “rules” or well-intentioned advices how to code the software. Every letter stands for one principle.

  • S for Single Responsibility

  • O for Open-Closed

  • L for Liskov Substitution

  • I for Interface Segregation

  • D for Dependency Inversion

Let’s go through it one by one …

Single Responsibility Principle

“A class should only have one responsibility and it should only have one reason to change.” - Robert C. Martin

HOW TO VIOLATE THIS PRINCIPLE

Let’s think about a class called Animal.

<?php

class Animal
{
    private string $name;
    private int $legs;
}

And now the developer wants to make some DB actions in this class because he could think about: “Hey, I have everything within this class, I could directly include everything which I need to save the animal to the DB or read some animal facts out of the DB.”

<?php

class Animal
{
    private string $name;
    private int $legs;
    
    public function __construct(private MySQLConnetion $mySQLConnetion)
    {}
    
    public function saveAnimal(): void
    {
        // save animal to the database
    }
    
    public function getAllAnimalsWithOneLeg(): array
    {
        // get all one legged animals
    }
}

Yes, it will work perfectly but leads to some challenges.

It’s not reusable for other classes or objects on the clean way because the whole logic is here interconnected.
The more responsibilities the class has, the more often it needs to be changed. When the codebase grows, also the logic grows.
Each change is more complicated, has more side effects and requires a lot more work than it should have. It’s a hard time for fixing errors.

Don’t do it like this!

HOW TO DO IT BETTER

Just separating the tasks for the DB in an extra class named for example AnimalDB and leaving the core class Animal untouched.

<?php

class AnimalDB
{
    public function __construct(private MySQLConnetion $mySQLConnetion)
    {}
    
    public function saveAnimal(): void
    {
        // save animal to the database
    }
    
    public function getAllAnimalsWithOneLeg(): array
    {
        // get all one legged animals
    }
}

TAKE AWAYS
Now that logic is being separated, the code is easier to understand. Each core functionality has its own class. Classes can be tested more efficiently.
Easily to maintain and scalable because instead of reading interconnected lines of code, the concerns have been separated.
Now the focus can be on the features which are important to work on. Really easy example, but imagine to follow this principle on a really complex task. This will definitely be helpful.

Open-Closed Principle

“Objects or entities should be open for extension but closed for modification.” - Bertrand Meyed

HOW TO VIOLATE THIS PRINCIPLE

In this example there are two animal classes “Dog” and “Duck”.

<?php
    
class Dog
{
    public function doBark(): string
    {
        return 'wuff wuff';
    }
}

class Duck
{
    public function doQuack(): string
    {
        return 'quack quack';
    }
}

Each class has its own “sound-function” which returns a string about the sound the animal makes.

There is also an AnimalSoundMachine class which is responsible to play the sound of each given animal.

<?php

class AnimalSoundMachine
{
    public function doTheAnimalSound($animal): string
    {
        if($animal instanceof Dog) {
            return $animal->doBark();
        } elseif ($animal instanceof Duck) {
            return $animal->doQuack();
        } else {
            throw new \InvalidArgumentException('Unknown animal');
        }
    }
}

Ok, is this class opened or closed for modification?

So for every new animal, a new logic must be added to the “doTheAnimalSound”- function. This is quite a simple example and easy to handle on this state of a function.

But when the application grows and becomes more complex, a new state of “elseif” has to be added in the function over and over again in the “doTheAnimalSound”- function each time a new animal is added, all over the application.
Definitely not closed for modification!

Don’t do it like this!

HOW TO DO IT BETTER

Let’s create for example an interface which all animal classes are going to implement.

<?php

interface AnimalSoundInterface
{
    public function makeSound(): string;
}

class Dog implements AnimalSoundInterface
{
    public function makeSound(): string
    {
        return 'wuff wuff';
    }
}

class Duck implements AnimalSoundInterface
{
    public function makeSound(): string
    {
        return 'quack quack';
    }
}

Animal classes that implement the interface, are forced to implement the declared function “makeSound” from this interface.
With this improvement, our AnimalSoundMachine class looks much easier.

<?php

class AnimalSoundMachine
{
    public function doTheAnimalSound(AnimalSoundInterface $animal): string
    {
        return $animal->makeSound();
    }
}

Every animal that implements the AnimalSoundInterface has the function makeSound. No changes need to be made on the basic functionality if there will be new animals with new sounds.
Typehinting the insert $animal variable with the interface will ensure, that every new animal has the function makeSound.
So closed for modification!.

TAKE AWAYS
Summarized, this principle is helping to add new functionality without changing the existing code. Writing the code in a way, that the functionality will be able to extended without changing the base functionality is the goal.
It also avoids situations in which a change to one of the classes also requires to adapt all the depending classes.
And it forces to use abstractions like interfaces. They can easily be substituted without changing the code that uses them.

Liskov Substitution Principle

“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.” - Barbara Liskov

HOW TO VIOLATE THIS PRINCIPLE

In this case there will be an abstract class Bird which has 2 functions: fly() and walk().

<?php

// parent class
abstract class Bird
{
    public function fly(): void
    {
        // fly away
    }

    public function walk(): void
    {
        // go for a chilling walk
    }
}

class Duck extends Bird
{
    public function walk(): void
    {
        // toddle slowly around like a duck
    }
}

The class Duck will extend the class Bird and overwrites the walk function with its own tasks and also with the same behaviour as the parent class! The fly() function is automatically being accessible by the Duck because of the extended Bird class.

Let’s think of a function letTheBirdFly() in another class. The variable $duck is type-hinted with the abstract class, and easily the fly() function for example can be called on a generated instance of the Duck.

<?php

$duck = new Duck();

// .... imagine in a class there will be this function
public function letTheBirdFly(Bird $duck): void {
    $duck->fly();
}

No problems….. until NOW:

Do you know what an ostrich is? It’s a bird…. but it cannot fly! BUT let’s say…it’s a bird.
So let’s create a new class.

<?php
// child class
class Ostrich extends Bird
{
    public function walk(): void
    {
        // do the ostrich run like a lightning
    }

    public function fly(): void
    {
        throw new \Exception('Unfortunately I am not able to fly');
    }
}

As mentioned, an ostrich can not fly! So the fly() function from the parent class must be overwritten by the child Ostrich class.

And the Ostrich has to throw an exception because of its not-flying-body.

But then, the behaviour of the parent class is not anymore the behaviour of the child class.

What happens now?

<?php

$bird = new Ostrich();

// .... imagine in a class there will be this function
public function letTheBirdFly(Bird $bird) 
{
    $bird->fly(); // throws an Exception
}

It’s not sure anymore, that the letTheBirdFly() function will act as expected. It will not make the bird fly, it will make the bird falling to the ground in a hard way.

Don’t do it like this!

HOW TO DO IT BETTER

Let’s create two abstractions. Now the class Bird only has the walk() function. So the Ostrich extends directly this class. The new class FlyingBird will only have the fly() function and will extend the Bird class. Then our Duck will extend the FlyingBird class and will have automatically inherited the Bird class.

The two abstract classes could look like this:

<?php

abstract class Bird
{
    public function walk(): void
    {
        // go for a chilling walk
    }
}

abstract class FlyingBird extends Bird
{
    public function fly(): void
    {
        // fly away
    }
}

And the two child classes:

<?php

class Duck extends FlyingBird
{
    public function walk(): void
    {
        // toddle slowly around like a duck
    }
}

class Ostrich extends Bird
{
    public function walk(): void
    {
        // do the ostrich run like a lightning
    }
}

Now Ostrich doesn’t have to overwrite the fly() function, because it does not exist anymore for it. So the child-classes can exactly act like their parent classes:

<?php

$ostrich = new Ostrich();
$duck = new Duck();

// ...in a class:
public function letTheBirdRun(Bird $ostrich) {
    $ostrich->walk();
}

public function letTheBirdRun(Bird $duck) {
    $duck->walk();
}

public function letTheBirdFly(FlyingBird $duck) {
    $duck->fly();
}

TAKE AWAYS
Child function arguments must match parent function arguments. Also the function return type must match the parent return type.
When exceptions are thrown by a child function, the exceptions must be the same or inherit from an exception which are thrown by the parent class.
Following these rules, will avoid unexpected issues during updates, extensions, changes or inheritance.

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 functions they do not use.” - Robert C. Martin

HOW TO VIOLATE THIS PRINCIPLE

In this case, let’s create an interface which could look like this one:

<?php

interface AnimalInterface
{
    public function swim(): void;
    public function run(): void;
    public function jump(): void;
}

And then there will be two animal classes which will implement this interface:

<?php

class Dog implements AnimalInterface
{
    public function swim(): void
    {
        // swim like a dog
    }
    
    public function run(): void
    {
        // run like a dog
    }
    
    public function jump(): void
    {
        // jump like a dog
    }
}

class Fish implements AnimalInterface
{
    public function swim(): void
    {
        // swim like a fish
    }
    
    public function run(): void
    {
        // run like a .... RUN??? How should I do this??
    }
    
    public function jump(): void
    {
        // ... not my favourite thing to do in the water
    }
}

Of course this can be done without any error. But a class should not be exposed to functions that it doesn’t need. Declaring functions in an interface that the client doesn’t need pollutes the interface and leads to bulky interfaces. So the goal here should be, to make small interfaces instead of big ones. And how it make sense for sure.

Don’t do it like this!

HOW TO DO IT BETTER

In this case, creating 2 interfaces where the actions belong together makes more sense:

<?php

interface LandMovementInterface
{
    public function run(): void;
    public function jump(): void;
}

interface WaterMovementInterface
{
    public function swim(): void;
}

With these two interfaces there can be made more exact assignments to the two animals:

<?php

class Dog implements LandMovementInterface, WaterMovementInterface
{
    public function swim(): void
    {
        // swim like a dog
    }
    
    public function run(): void
    {
        // run like a dog
    }
    
    public function jump(): void
    {
        // jump like a dog
    }
}

class Fish implements WaterMovementInterface
{
    public function swim(): void
    {
        // swim like a fish
    }
}

Now every animal has only to implement the functions which should be really needed for its case.

TAKE AWAYS
Make small instead of big interfaces. When creating an interfaces, make sure the interfaces are like classes, and have a single responsibility.
Don’t force a Class to implement a function it has no use for. This also prevents code smell. Think of, changing or recompiling all the clients of that interface, even if they are unrelated to some functions.
And of course, smaller interfaces have more reusability. If the interface has “one job” to do, it can easily be implemented to classes which also has the needs of these tasks

Dependency Inversion Principle

“A. High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
B. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.”
- Robert C. Martin

HOW TO VIOLATE THIS PRINCIPLE

Let’s have a look at these two classes:

<?php

class MySqlConnection
{
    public function connect()
    {
        // connect to MySQL Database
    }
}

class AnimalCounter
{
    public function construct(private MySqlConnection $mySqlConnection)
    {}
    
    public function countAllAnimals(): int
    {
        // count all animals in the database
    }
    
    public function getAllAnimalsWithOneLeg(): array
    {
        // make database query to get all one legged animals
    }
}

Also this example will work without any problems… until there will be a change on the MySqlConnection for example to a PostGresDatabaseConnection.
Because of the hard implementation of the MySqlConnection class in the AnimalCounter constructor, there must be an edit at this class when changing the connection.
So this means also violating the Open/Closed principle.

Don’t do it like this!

HOW TO DO IT BETTER

Making it more flexible:

<?php

interface DBConnectionInterface
{
    public function connect();
}

class MySqlConnection implements DBConnectionInterface
{
    public function connect()
    {
        // connect to MySQL Database
    }
}

class AnimalCounter
{
    public function construct(private DBConnectionInterface $mySqlConnection)
    {}
    
    public function countAllAnimals(): int
    {
        // count all animals in the database
    }
    
    public function getAllAnimalsWithOneLeg(): array
    {
        // make database query to get all one legged animals
    }
}

Yes there is still a dependency, but on another hierarchy level.
But there is no dependency on the base class “MySqlConnection”, only of an abstraction of this class in form of the interface “DBConnectionInterface”.
And now it’s not important anymore which DatabaseType the class is using as long this class includes the new generated interface. So the base class is interchangeable.
By consequently applying the Open/Closed Principle and the Liskov Substitution Principle to the code, it will also follow the Dependency Inversion Principle.

TAKE AWAYS
The lower the module in the hierarchy, the more specific problems it solves. Changes in a class from a deep level could affect all levels above itself, if this principle will not be followed.
The goal should be to depend on abstractions, not on concretions. So it should be considered, for which basic tasks the class will be responsible for.
Following these rule offers a way to decouple software modules. It makes the code more flexible, agile and reusable.

SOLID Conclusion

These 5 principles are no rocket science. But to follow them strictly in projects is the big job. And it’s really hard to follow these during the development.

They are no 1:1 ruleset – more or less they are a formula collection of well-intentioned advices that should help during the development.
It’s important to keep in mind that the SOLID principles are just that: principles.
They’re not some kind of magic recipe that will lead you to the promised land of milk and honey or perfect software.
You still have to study and to practice, and they are not a cure-all and won’t avoid design issues.

It’s also a matter of interpretation. For example, we talked about the “Single Responsibility Principle”. Every class should have only one task for which it’s intended for.
But then you have to look for yourself: How deep can I make it generic? What’s the smallest function this class should have? So totally interpretation and depending on your case.

Changes do happen during the development. We cannot avoid it or can sometimes imagine what will happen. But when that happens, maybe the new requirements will make our original designs no longer adhere to SOLID principles.
It’s a process which has to be followed again and again.

You have to re-SOLID your code.

Use your discretion as to how, where and when to apply these rules. They are coming with many benefits, but following these principles generally leads to write longer and more complex code.
But the effort is well worth! It makes software so much easier to maintain, test, and extend.

So it’s your code, you have to decide whether to choose one or all of these principles.

And maybe… it will help you to become a better developer with SOLID principles.