SOLID is an acronym for the first five object-oriented design (OOD) principles by Robert C. Martin (also known as Uncle Bob).
These principles establish practices that lend to developing software with considerations for maintaining and extending as the project grows. Adopting these practices can also contribute to avoiding code smells, refactoring code, and Agile or Adaptive software development.
SOLID stands for:
S - Single-responsiblity Principle
O - Open-closed Principle
L - Liskov Substitution Principle
I - Interface Segregation Principle
D - Dependency Inversion Principle
Now let's talk about the first one:
Single Responsibility Principle
"A Class Should Have One, And Only One Responsibility"
It means that if our class assumes more than one responsibility we will have a high coupling. The cause is that our code will be fragile at any changes.
Suppose we have a User class like the following:
<?php
class User {
private $email;
// Getter and setter...
public function store() {
// Store attributes into a database...
}
}
In this case, the method store
is out of the scope and this responsibility should belong to a class that manages the database.
The solution here is to create two classes each with proper responsibilities.
<?php
class User {
private $email;
// Getter and setter...
}
class UserDB {
public function store(User $user) {
// Store the user into a database...
}
}
Now let's move on to the O in SOLID
Open-closed Principle
Objects or entities should be open for extension but closed for modification.
According to this principle, a software entity must be easily extensible with new features without having to modify its existing code in use.
Suppose we have to calculate the total area of some objects and to do that we need an AreaCalculator
class that does only a sum of each shape area.
The issue here is that each shape has a different method to calculate its own area.
<?php
class Rectangle {
public $width;
public $height;
public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}
}
class Square {
public $length;
public function __construct($length) {
$this->length = $length;
}
}
class AreaCalculator {
protected $shapes;
public function __construct($shapes = array()) {
$this->shapes = $shapes;
}
public function sum() {
$area = [];
foreach($this->shapes as $shape) {
if($shape instanceof Square) {
$area[] = pow($shape->length, 2);
} else if($shape instanceof Rectangle) {
$area[] = $shape->width * $shape->height;
}
}
return array_sum($area);
}
}
If we add another shape like a Circle
we have to change the AreaCalculator
in order to calculate the new shape area and this is not sustainable.
The solution here is to create a simple Shape
interface that has the area method and will be implemented by all other shapes.
<?php
interface Shape {
public function area();
}
class Rectangle implements Shape {
private $width;
private $height;
public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}
public function area() {
return $this->width * $this->height;
}
}
class Square implements Shape {
private $length;
public function __construct($length) {
$this->length = $length;
}
public function area() {
return pow($this->length, 2);
}
}
class AreaCalculator {
protected $shapes;
public function __construct($shapes = array()) {
$this->shapes = $shapes;
}
public function sum() {
$area = [];
foreach($this->shapes as $shape) {
$area[] = $shape->area();
}
return array_sum($area);
}
}
In this way, we will use only one method to calculate the sum and if we need to add a new shape it will just implement the Shape
interface.
The third principle in SOLID L stands for:
Liskov Substition Princple
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.
The principle says that objects must be replaceable by instances of their subtypes without altering the correct functioning of our system.
I know this is hard to understand so I separated the meaning in 5 parts.
1. Child function arguments must match function arguments of parent
2. Child function return type must match parent function return type
3. Child pre-conditions cannot be greater than parent function pre-conditions
4. Child function post-conditions cannot be lesser than parent function post-conditions.
5. Exceptions thrown by child method must be the same as or inherit from an exception thrown by the parent method.
To fully understand this here's a scenario:
Imagine managing two types of coffee machine. According to the user plan, we will use a basic or a premium coffee machine, the only difference is that the premium machine makes a good vanilla coffee plus than the basic machine.
<?php
interface CoffeeMachineInterface {
public function brewCoffee($selection);
}
class BasicCoffeeMachine implements CoffeeMachineInterface {
public function brewCoffee($selection) {
switch ($selection) {
case 'ESPRESSO':
return $this->brewEspresso();
default:
throw new CoffeeException('Selection not supported');
}
}
protected function brewEspresso() {
// Brew an espresso...
}
}
class PremiumCoffeeMachine extends BasicCoffeeMachine {
public function brewCoffee($selection) {
switch ($selection) {
case 'ESPRESSO':
return $this->brewEspresso();
case 'VANILLA':
return $this->brewVanillaCoffee();
default:
throw new CoffeeException('Selection not supported');
}
}
protected function brewVanillaCoffee() {
// Brew a vanilla coffee...
}
}
function getCoffeeMachine(User $user) {
switch ($user->getPlan()) {
case 'PREMIUM':
return new PremiumCoffeeMachine();
case 'BASIC':
default:
return new BasicCoffeeMachine();
}
}
function prepareCoffee(User $user, $selection) {
$coffeeMachine = getCoffeeMachine($user);
return $coffeeMachine->brewCoffee($selection);
}
The main program behavior must be the same for both machines.
The fourth principle I stands for:
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 methods they do not use.
This principle defines that a class should never implement an interface that does not go to use.
In that case, means that in our implementations we will have methods that don’t need.
The solution is to develop specific interfaces instead of general-purpose interfaces.
Here's a scenario, imagine we invent the FutureCar
that can both fly and drive…
<?php
interface VehicleInterface {
public function drive();
public function fly();
}
class FutureCar implements VehicleInterface {
public function drive() {
echo 'Driving a future car!';
}
public function fly() {
echo 'Flying a future car!';
}
}
class Car implements VehicleInterface {
public function drive() {
echo 'Driving a car!';
}
public function fly() {
throw new Exception('Not implemented method');
}
}
class Airplane implements VehicleInterface {
public function drive() {
throw new Exception('Not implemented method');
}
public function fly() {
echo 'Flying an airplane!';
}
}
The main issue, as you can see, is that the Car and Airplane
have methods that don’t use.
The solution is to split the VehicleInterface
into two more specific interfaces that are used only when it’s necessary, like the following:
<?php
interface CarInterface {
public function drive();
}
interface AirplaneInterface {
public function fly();
}
class FutureCar implements CarInterface, AirplaneInterface {
public function drive() {
echo 'Driving a future car!';
}
public function fly() {
echo 'Flying a future car!';
}
}
class Car implements CarInterface {
public function drive() {
echo 'Driving a car!';
}
}
class Airplane implements AirplaneInterface {
public function fly() {
echo 'Flying an airplane!';
}
}
Last but not the least is D which stands for:
Dependency Inversion Principle
Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions.
This principle means that a particular class should not depend directly on another class but on an abstraction of this class.
This principle allows for decoupling and more code reusability.
Let’s get the first example of the UserDB
class. This class could depend on a DB connection:
<?php
class UserDB {
private $dbConnection;
public function __construct(MySQLConnection $dbConnection) {
$this->$dbConnection = $dbConnection;
}
public function store(User $user) {
// Store the user into a database...
}
}
In this case, the UserDB
class depends directly from the MySQL database.
That means that if we would change the database engine in use we need to rewrite this class and violate the Open-Close Principle.
The solution is to develop an abstraction of database connection:
<?php
interface DBConnectionInterface {
public function connect();
}
class MySQLConnection implements DBConnectionInterface {
public function connect() {
// Return the MySQL connection...
}
}
class UserDB {
private $dbConnection;
public function __construct(DBConnectionInterface $dbConnection) {
$this->dbConnection = $dbConnection;
}
public function store(User $user) {
// Store the user into a database...
}
}
This code establishes that both the high-level and low-level modules depend on abstraction.
Hurray! We are done with the 5 SOLID Principle of Object Oriented Programming! You can use these principles represent the state of the art of the code quality and following them permit you to write software that will be easily extended, reusable and refactored.
Projects that adhere to SOLID principles can be shared with collaborators, extended, modified, tested, and refactored with fewer complications.
Top comments (0)