SOLID Principles in Object-Oriented Programming: A Practical Guide with Examples
Learn the SOLID principles of object-oriented programming—Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion—with clear PHP examples to improve code maintainability and flexibility.
Understanding and applying the SOLID principles is essential for writing clean, maintainable, and scalable object-oriented code. These five principles, introduced by Robert C. Martin (also known as Uncle Bob), provide a framework for designing robust software architectures that can evolve without turning into a tangled mess. In this guide, we'll explore each SOLID principle through practical PHP examples and explain why they matter for long-term software success.
You can access the full code examples in this GitHub repository.
What Are the SOLID Principles?
The acronym SOLID stands for five foundational object-oriented design principles:
- S – Single Responsibility Principle
- O – Open/Closed Principle
- L – Liskov Substitution Principle
- I – Interface Segregation Principle
- D – Dependency Inversion Principle
These principles aim to solve common challenges in software maintenance, including tightly coupled code, poor scalability, and difficult testing.
Let’s explore each principle with code examples.
Single Responsibility Principle (SRP)
A class or function should have only one reason to change. This means it should focus on a single job or responsibility.
By narrowing the scope of a class:
- Maintenance becomes simpler
- Reusability increases
- Unit testing becomes more efficient
Anti-pattern example
class Singer {
public function sing() {
echo "🎶🎵";
}
public function writeSongs() {
echo "I'm writing songs of my album";
}
public function publishSong() {
echo "I'm publishing the song on Spotify";
}
}
Here, the Singer
class handles multiple responsibilities: performing, writing, and publishing. This violates SRP.
Refactored example
class Singer {
public function sing() {
echo "🎶🎵";
}
public function writeSongs() {
echo "I'm writing songs of my album";
}
}
class Producer {
public function publishSong() {
echo "I'm publishing the song on Spotify";
}
}
Each class now handles a distinct concern, aligning with SRP.
Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification. This encourages writing code that allows behavior to be added without altering existing source code.
Why OCP Matters
- Prevents unintended side effects from modifying legacy code
- Encourages the use of polymorphism and abstraction
Non-OCP approach
class AreaCalculation {
public function getAreaSum(array $shapes): float {
$sum = 0;
foreach($shapes as $shape) {
if($shape instanceof Triangle) {
$sum += ($shape->getBaseLength() * $shape->getHeight()) / 2;
} else if($shape instanceof Circle) {
$sum += pi() * ($shape->getRadius() ** 2);
}
}
return $sum;
}
}
Each new shape requires a change in AreaCalculation
.
OCP-compliant solution
abstract class Shape {
public abstract function getArea();
}
class Triangle extends Shape {
public function getArea(): float {
return ($this->getBaseLength() * $this->getHeight()) / 2;
}
}
class Circle extends Shape {
public function getArea(): float {
return pi() * ($this->getRadius() ** 2);
}
}
class AreaCalculation {
public function getAreaSum(array $shapes): float {
$sum = 0;
foreach($shapes as $shape) {
$sum += $shape->getArea();
}
return $sum;
}
}
Now new shapes can be added without modifying AreaCalculation
.
Liskov Substitution Principle (LSP)
Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.
Benefits of LSP
- Promotes consistent behavior across class hierarchies
- Reduces the risk of runtime errors
Violating LSP
abstract class Animal {
public abstract function makeNoise();
}
class Fish extends Animal {
public function makeNoise() {
throw new Exception("A fish can't make a noise under water");
}
}
This causes runtime issues if a Fish
is used in place of Animal
.
LSP-friendly design
abstract class LandAnimal {
public abstract function makeNoise();
}
abstract class AquaticAnimal {
public abstract function swim();
}
class Dog extends LandAnimal {
public function makeNoise() {
echo "Waf !";
}
}
class Fish extends AquaticAnimal {
public function swim() {
echo "I'm swimming with my fins";
}
}
Each subclass behaves consistently with its parent class, upholding LSP.
Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they do not use. In practice, this means interfaces should be narrowly defined.
Why ISP Matters
- Prevents bloated interfaces
- Encourages implementation clarity
Problematic interface
interface IFlyingMachine {
public function startEngines();
public function fly();
}
class Glider implements IFlyingMachine {
public function startEngines() {
throw new Exception("Sorry, I don't have engines");
}
public function fly() {
echo "The glider is flying above the sea";
}
}
Refactored with ISP
interface IFlyingMachine {
public function fly();
}
interface IFlyingMachineWithEngine {
public function startEngines();
}
class Airplane implements IFlyingMachine, IFlyingMachineWithEngine {
public function startEngines() {
echo "I start all the engines";
}
public function fly() {
echo "The airplane is flying above the sea";
}
}
class Glider implements IFlyingMachine {
public function fly() {
echo "The glider is flying above the sea";
}
}
Each class now implements only the methods relevant to its function.
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
This principle encourages the use of interfaces or abstract classes to reduce tight coupling.
Why DIP Matters
- Increases flexibility and reusability
- Simplifies testing and future development
Tightly coupled code
function foo(Human $thing) {
$thing->walk();
}
This depends on a concrete class (Human
), which limits flexibility.
Using dependency inversion
function foo(PersonInterface $thing) {
$thing->walk();
}
Now the function depends on an abstraction (PersonInterface
), not an implementation.
Final Thoughts
Mastering the SOLID principles is a key step toward becoming a proficient object-oriented programmer. These principles aren’t just theoretical—they offer practical benefits like improved testability, maintainability, and scalability. When consistently applied, they help teams write cleaner code, reduce bugs, and build software that adapts to change.
Explore more about software architecture, object-oriented best practices, and real-world PHP projects by checking out our other articles. For full code examples from this guide, visit the GitHub repository.