L o a d i n g
SOLID Principles: A Simple and Easy Explanation

SOLID Principles: A Simple and Easy Explanation

0

Vote for this post

Click the arrows to vote • 1 vote per logged in user
Login to Vote

Solid like a rock! SOLID Principles: A simple and easy explanation

SOLID is a coding standard that every developer should understand clearly when building software in a proper, maintainable way. Promoted by Robert C. Martin and used across the object-oriented design spectrum, these five principles – when applied consistently – make your code more extendable, logical, and easier to read.

When a developer builds software without following a disciplined design approach, the code can become inflexible and brittle. Small changes can introduce bugs across unrelated parts of the system. SOLID is the answer to that problem – not a theoretical exercise, but a practical discipline that produces code you can actually work with. It takes time to internalise, but once you do, you will find these principles embedded in the architecture of every well-designed system you encounter.

SOLID Principles overview diagram

The five SOLID principles form a unified approach to object-oriented design that keeps code clean, extendable, and resilient to change.


The Five Principles at a Glance

Each letter in SOLID maps to a distinct principle. Together they form a coherent framework for writing classes and modules that are easy to test, extend, and maintain. To understand SOLID fully, you need a clear concept of how interfaces work – they are the mechanism behind several of the principles.

Letter Principle Core Idea
S Single Responsibility Principle A class should have one, and only one, reason to change
O Open-Closed Principle Open for extension, closed for modification
L Liskov Substitution Principle Derived classes must be substitutable for their base classes
I Interface Segregation Principle A client should not be forced to implement interfaces it does not use
D Dependency Inversion Principle Depend on abstractions, not on concretions

S – Single Responsibility Principle

The Rule

A class should have one, and only one, reason to change.

One class should serve one purpose. This does not mean each class can only have one method – it means all methods and properties should work towards the same goal. When a class serves multiple responsibilities, those responsibilities should each live in their own dedicated class.

The OrdersReport class below does three different things: retrieving data from a database, formatting output as HTML, and combining those concerns in a single method. Any one of those concerns could independently force you to change this class – a clear sign it is carrying too much responsibility:

PHP – Violates SRP
namespace Demo;
use DB;
class OrdersReport
{
    public function getOrdersInfo($startDate, $endDate)
    {
        $orders = $this->queryDBForOrders($startDate, $endDate);
        return $this->format($orders);
    }
    protected function queryDBForOrders($startDate, $endDate)
    {
        return DB::table('orders')
            ->whereBetween('created_at', [$startDate, $endDate])
            ->get();
    }
    protected function format($orders)
    {
        return '<h1>Orders: ' . $orders . '</h1>';
    }
}

The refactored version separates each concern into its own class. The repository handles persistence, the formatter handles output, and the report class simply orchestrates. Now each class has exactly one reason to change:

PHP – Follows SRP
namespace Report;
use Report\Repositories\OrdersRepository;

class OrdersReport
{
    protected $repo;
    protected $formatter;

    public function __construct(OrdersRepository $repo, OrdersOutPutInterface $formatter)
    {
        $this->repo = $repo;
        $this->formatter = $formatter;
    }

    public function getOrdersInfo($startDate, $endDate)
    {
        $orders = $this->repo->getOrdersWithDate($startDate, $endDate);
        return $this->formatter->output($orders);
    }
}

namespace Report;
interface OrdersOutPutInterface
{
    public function output($orders);
}

namespace Report;
class HtmlOutput implements OrdersOutPutInterface
{
    public function output($orders)
    {
        return '<h1>Orders: ' . $orders . '</h1>';
    }
}

namespace Report\Repositories;
use DB;
class OrdersRepository
{
    public function getOrdersWithDate($startDate, $endDate)
    {
        return DB::table('orders')
            ->whereBetween('created_at', [$startDate, $endDate])
            ->get();
    }
}

O – Open-Closed Principle

The Rule

Software entities should be open for extension, but closed for modification.

Classes, modules, and functions should be extendable without requiring you to change the original code. If you follow this principle consistently, you can modify system behaviour by adding new code – not by editing existing, tested code that already works. The aim is to avoid touching a class that already does its job correctly.

In the example below, adding a new shape like Square forces you to modify the calculate method in CostManager. That breaks the Open-Closed Principle – the class is not closed for modification:

PHP – Violates OCP
class CostManager
{
    public function calculate($shape)
    {
        $costPerUnit = 1.5;
        if ($shape instanceof Rectangle) {
            $area = $shape->width * $shape->height;
        } else {
            $area = $shape->radius * $shape->radius * pi();
        }
        return $costPerUnit * $area;
    }
}

With the Open-Closed approach, each shape implements its own calculateArea() method via an interface. CostManager never needs to change when a new shape is added:

PHP – Follows OCP
interface AreaInterface
{
    public function calculateArea();
}

class Rectangle implements AreaInterface
{
    public $width;
    public $height;

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

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

class Circle implements AreaInterface
{
    public $radius;

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

    public function calculateArea()
    {
        return $this->radius * $this->radius * pi();
    }
}

class CostManager
{
    public function calculate(AreaInterface $shape)
    {
        $costPerUnit = 1.5;
        return $costPerUnit * $shape->calculateArea();
    }
}

L – Liskov Substitution Principle

The Rule

Subclasses and derived classes should be substitutable for their base or parent class.

The Liskov Substitution Principle was introduced by Barbara Liskov in her 1987 conference keynote and formalised alongside Jeannette Wing in a 1994 paper. It states that any implementation of an abstraction – an interface – should be substitutable in any place that abstraction is accepted. Practically, this means that different classes implementing the same interface should not only accept the same inputs but return the same type of output.

The violation below is subtle but dangerous. Both classes implement the same interface, but DbLessonRepository originally returned an Eloquent Collection – a different type from the plain array returned by FileLessonRepository. Any consumer expecting an array would break silently at runtime:

PHP – LSP Violation & Fix
interface LessonRepositoryInterface
{
    public function getAll();
}

class FileLessonRepository implements LessonRepositoryInterface
{
    public function getAll()
    {
        return [];  // returns array
    }
}

class DbLessonRepository implements LessonRepositoryInterface
{
    public function getAll()
    {
        // Violation: Eloquent Collection, not an array
        // return Lesson::all();

        // Fix: normalise the return type to match the contract
        return Lesson::all()->toArray();
    }
}
Interfaces and substitutability in object-oriented design

Interfaces define the contract. Liskov Substitution ensures every implementation of that contract behaves predictably – consumers should never need to know which concrete class they are working with.


I – Interface Segregation Principle

The Rule

A client should not be forced to implement an interface that it does not use.

Break large interfaces into smaller, more focused ones so each client only depends on what it actually needs. The goal is the same as the Single Responsibility Principle – minimise side effects and eliminate unnecessary coupling by keeping each interface narrow and purposeful. Similar to SRP, the Interface Segregation Principle divides the software into multiple independent parts.

In the example below, RobotWorker is forced to implement sleep() even though it has no use for it. That is a clear violation of ISP:

PHP – Violates ISP
interface WorkerInterface
{
    public function work();
    public function sleep();
}

class HumanWorker implements WorkerInterface
{
    public function work()
    {
        var_dump('works');
    }

    public function sleep()
    {
        var_dump('sleep');
    }
}

class RobotWorker implements WorkerInterface
{
    public function work()
    {
        var_dump('works');
    }

    public function sleep()
    {
        // Not needed -- forced to implement anyway
    }
}

Splitting into two focused interfaces solves it immediately. RobotWorker implements only WorkAbleInterface – the one thing it actually needs:

PHP – Follows ISP
interface WorkAbleInterface
{
    public function work();
}

interface SleepAbleInterface
{
    public function sleep();
}

class HumanWorker implements WorkAbleInterface, SleepAbleInterface
{
    public function work()
    {
        var_dump('works');
    }

    public function sleep()
    {
        var_dump('sleep');
    }
}

class RobotWorker implements WorkAbleInterface
{
    public function work()
    {
        var_dump('works');
    }
}

D – Dependency Inversion Principle

The Rule

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details – details should depend on abstractions.

This principle is frequently confused with dependency injection, but they are not the same. Dependency injection is a technique for supplying dependencies. Dependency Inversion is a design rule about what those dependencies should point at: abstractions, not concrete implementations.

In the violation below, PasswordReminder depends directly on MySQLConnection. Switching to MongoDB would require editing the high-level class, which has nothing to do with the choice of database:

PHP – Violates DIP
class MySQLConnection
{
    public function connect()
    {
        var_dump('MYSQL Connection');
    }
}

class PasswordReminder
{
    private $dbConnection;

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

By introducing a ConnectionInterface, both the high-level and low-level modules depend on the abstraction. You can now swap MySQLConnection for any other connection class without touching PasswordReminder at all:

PHP – Follows DIP
interface ConnectionInterface
{
    public function connect();
}

class DbConnection implements ConnectionInterface
{
    public function connect()
    {
        var_dump('MYSQL Connection');
    }
}

class PasswordReminder
{
    private $dbConnection;

    public function __construct(ConnectionInterface $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}
Clean architecture applying SOLID principles

Applied together, the SOLID principles produce a codebase where each module has a single clear purpose, dependencies point toward abstractions, and new behaviour can be added without touching existing code.


Signs You Are Following SOLID – and Signs You Are Not

SOLID in Practice: Healthy vs. Problem Signals

  • A class handles data retrieval, formatting, and business logic all in one place – multiple reasons to change
  • Adding a new type or category requires editing an existing working class rather than extending it
  • Two classes implement the same interface but return different types from the same method
  • A class implements interface methods it has no use for, leaving them empty or throwing exceptions
  • A high-level class has a hard-coded dependency on a specific database driver or external service
  • Tests are hard to write because classes pull in too many concrete dependencies
  • Each class has a single, clear purpose and only one reason to change
  • New behaviour is added by extending or implementing, not by editing code that already works
  • Any implementation of an interface can replace any other without breaking consumers
  • Interfaces are small and focused – clients only depend on what they actually use
  • High-level modules depend on interfaces, not concrete classes – swapping implementations requires no changes upstream
SOLID Is a Direction, Not a Destination

No real codebase applies every principle perfectly at every level. The value of SOLID is not in achieving theoretical purity – it is in the direction it pulls your design decisions. When you find yourself editing a class that already works because requirements changed elsewhere, that is a signal. When you find yourself implementing a method that does nothing because the interface demands it, that is a signal. SOLID gives you a name for what is wrong and a clear path toward what is better.


Further Reading

The SOLID principles were popularised by Robert C. Martin (Uncle Bob) in his writing and lectures on object-oriented design. The original formulation of the Liskov Substitution Principle comes from Barbara Liskov's 1987 conference keynote and her 1994 paper with Jeannette Wing. The following resources provide both the theory and the practical application of these principles:

0 Comments

    No Comment(s) found!! 😌😌

Leave a Comment