SOLID is an acronym representing five essential principles of object-oriented design (OOD) introduced by Robert C. Martin, also known as Uncle Bob. These principles provide guidelines for writing maintainable and scalable software, ensuring it remains flexible as the project evolves.
While these principles can be applied across various programming languages, the examples in this article will use PHP.
By following SOLID principles, you can avoid common pitfalls, improve code quality, and facilitate Agile or Adaptive software development. The five principles are:
- S: Single Responsibility Principle (SRP)
- O: Open-Closed Principle (OCP)
- L: Liskov Substitution Principle (LSP)
- I: Interface Segregation Principle (ISP)
- D: Dependency Inversion Principle (DIP)
Let’s look into each principle individually to see how they can make you a better developer.
Single Responsibility Principle (SRP)
The Single Responsibility Principle suggests that a class should have only one reason to change. In other words, a class should have only one responsibility.
Example:
Imagine an application that calculates the sum of areas for different shapes, such as circles and squares. Here’s a basic implementation:
class Square
{
public $length;
public function __construct($length)
{
$this->length = $length;
}
}
class Circle
{
public $radius;
public function __construct($radius)
{
$this->radius = $radius;
}
}
class AreaCalculator
{
protected $shapes;
public function __construct($shapes = [])
{
$this->shapes = $shapes;
}
public function sum()
{
foreach ($this->shapes as $shape) {
if (is_a($shape, ‘Square’)) {
$area[] = pow($shape->length, 2);
} elseif (is_a($shape, ‘Circle’)) {
$area[] = pi() * pow($shape->radius, 2);
}
}
return array_sum($area);
}
public function output()
{
return ‘Sum of the areas of provided shapes: ‘ . $this->sum();
}
}
The issue here is that the AreaCalculator class is responsible for both calculating areas and formatting the output. This violates the Single Responsibility Principle.
Fixing SRP:
To resolve this, we can extract the output logic into a separate class:
class SumCalculatorOutputter
{
protected $calculator;
public function __construct(AreaCalculator $calculator)
{
$this->calculator = $calculator;
}
public function JSON()
{
return json_encode([‘sum’ => $this->calculator->sum()]);
}
public function HTML()
{
return ‘Sum of the areas of provided shapes: ‘ . $this->calculator->sum();
}
}
Now, the AreaCalculator is only concerned with summing areas, and the output logic is delegated to the SumCalculatorOutputter.
Open-Closed Principle (OCP)
The Open-Closed Principle dictates that classes should be open for extension but closed for modification. This means you should be able to extend a class’s functionality without changing its existing code.
Example:
Consider the AreaCalculator class again. If you need to support additional shapes (e.g., triangles or pentagons), you’d typically add more conditions to the sum() method. However, this violates the OCP since the class is not closed for modification.
Fixing OCP:
Instead of modifying AreaCalculator, we can move the area calculation logic into each shape class. Each shape can define its area() method:
class Square
{
public $length;
public function __construct($length)
{
$this->length = $length;
}
public function area()
{
return pow($this->length, 2);
}
}
class Circle
{
public $radius;
public function __construct($radius)
{
$this->radius = $radius;
}
public function area()
{
return pi() * pow($this->radius, 2);
}
}
class AreaCalculator
{
protected $shapes;
public function __construct($shapes = [])
{
$this->shapes = $shapes;
}
public function sum()
{
$area = 0;
foreach ($this->shapes as $shape) {
$area += $shape->area();
}
return $area;
}
}
Now, you can add new shape classes (e.g., Triangle) without modifying the AreaCalculator class.
Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that objects of a subclass should be replaceable by objects of the superclass without affecting the functionality.
Example:
Let’s assume you have a VolumeCalculator that extends AreaCalculator:
class VolumeCalculator extends AreaCalculator
{
public function sum()
{
// Logic for calculating volumes
return $summedVolume;
}
}
However, if you pass a VolumeCalculator object to a method expecting an AreaCalculator, it might break functionality. This happens because the VolumeCalculator does not return the same type of data (e.g., it might return an array instead of a single value).
Fixing LSP:
To adhere to LSP, ensure that subclasses return values consistent with their parent class and don’t alter expected behavior. This can be done by modifying the sum() method of VolumeCalculator to return a single value (e.g., a float or integer).
Interface Segregation Principle (ISP)
The Interface Segregation Principle states that a class should not be forced to implement methods it doesn’t use. Clients should depend only on the interfaces they use.
Example:
If you add a volume() method to the ShapeInterface, shapes like Square (which doesn’t have a volume) would be forced to implement this method, violating ISP.
Fixing ISP:
Instead of adding volume() to the ShapeInterface, create a separate ThreeDimensionalShapeInterface for shapes that require volume calculation:
interface ShapeInterface
{
public function area();
}
interface ThreeDimensionalShapeInterface
{
public function volume();
}
class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface
{
public function area()
{
// Calculate surface area
}
public function volume()
{
// Calculate volume
}
}
Now, only shapes that need a volume() method will implement the ThreeDimensionalShapeInterface.
Dependency Inversion Principle (DIP)
The Dependency Inversion Principle states that high-level modules should not depend on low-level modules; both should depend on abstractions.
Example:
Imagine a PasswordReminder class that directly depends on a MySQLConnection:
class PasswordReminder
{
private $dbConnection;
public function __construct(MySQLConnection $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
This violates DIP because PasswordReminder depends on the concrete MySQLConnection class.
Fixing DIP:
Instead, define an abstraction (DBConnectionInterface), and have PasswordReminder depend on it:
interface DBConnectionInterface
{
public function connect();
}
class MySQLConnection implements DBConnectionInterface
{
public function connect()
{
return ‘Connected to MySQL database’;
}
}
class PasswordReminder
{
private $dbConnection;
public function __construct(DBConnectionInterface $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
Now, PasswordReminder can work with any database that implements DBConnectionInterface, making it more flexible and decoupled from specific implementations.
Final Words
By adopting the SOLID principles, developers can create software that is more maintainable, extendable, and adaptable to future changes. These principles help you write cleaner code, reduce bugs, and work more efficiently in collaborative environments.
Keep exploring and applying these principles to improve your coding practices and overall development skills.
FAQs
1. What is SOLID?
SOLID is a set of five design principles for maintainable and scalable software.
2. What is SRP?
SRP states that a class should have one responsibility.
3. What is OCP?
OCP means classes should be extendable but not modify existing code.
4. What is LSP?
LSP ensures subclasses can replace their parent class without breaking functionality.
5. What is ISP?
ISP ensures classes only implement methods they use.
6. What is DIP?
DIP means high-level modules depend on abstractions, not concrete classes.
7. How to apply SRP?
Make sure each class handles one task only.
8. How to follow OCP?
Extend classes via inheritance, not modification.
9. What is an LSP violation?
It occurs when a subclass changes expected behavior, e.g., returning the wrong type.
10. Why is ISP important?
ISP keeps classes focused on relevant methods, reducing complexity.
11. Why use DIP?
DIP makes your code flexible and easier to test.
12. How do SOLID principles help in Agile?
They ensure code is modular and adaptable to change.
13. What happens if I ignore SOLID?
Your code becomes hard to maintain and extend.
14. Are SOLID principles language-dependent?
No, they apply across object-oriented languages.
15. Should I always use SOLID?
Use SOLID for larger projects, but not always for smaller ones.