What is SOLID?
As a software developer, we always need to deal with changes in requirements or a new feature in the middle of development or some extension on a feature in the maintenance phase. It is important to write code that?s easy to extend and maintain and make our life less painful. SOLID is a set of rules that guide us on how to achieve that in object-oriented programming. Using SOLID guidelines you can avoid bad design in your code and give a well-structured architecture to your design. Bad design leads to the inflexible and brittle codebase, a simple and small change can result in bugs with bad design.
SOLID is an acronym of five sets of principles that was proposed by Robert C Martin aka ?Uncle Bob? in the year 2000 to write high cohesive, maintainable and extensible software systems.
Following are the five concepts that make up SOLID principles:
- Single Responsibility principle
- Open/Closed principle
- Liskov Substitution principle
- Interface Segregation principle
- Dependency Inversion principle.
So let?s understand each principle in detail with example
Single Responsibility Principle: As the name suggests, this principle?s purpose is to have a single responsibility for a class/module. In other words, we can say that the class or module should solve one and only one problem So it should have a single reason to change. It makes our code more cohesive hence make it easy to test and maintain.
?A class should only have a single responsibility, that is, only changes to one part of the software?s specification should be able to affect the specification of the class?
Let?s understand with a simple example:
class Logger{ private $logs = ; public function add($log){ $now = new DateTime(); $date = $now->format(“Y-m-d h:i:s.u”); $this->logs = $date.” : “.$log; } public function toString($dimiliter=”, “){ if(empty($this->logs)){ return “No logs”; } return implode($this->logs,$dimiliter); } public function reset(){ $this->logger=; }public function save($fileName){ $fp = fopen($fileName,”w”); fwrite($fp,$this->toString(“n”)); fclose($fp); } }$logger = new Logger();$logger->add(“First log”);$logger->add(“Second log”);$logger->add(“Third log”);$logger->save(“logs.txt”);
In the above example code, we have a simple Logger class that collects the logs and saves them to a given file name. Everything looks good but the above class breaks the single responsibility principle because the class Logger has two responsibilities: collecting logs and saving logs.Let?s see the right implementation
<?php class Logger{ private $logs = ; public function add($log){ $now = new DateTime(); $date = $now->format(“Y-m-d h:i:s.u”); $this->logs = $date.” : “.$log; } public function toString($dimiliter=”, “){ if(empty($this->logs)){ return “No logs”; } return implode($this->logs,$dimiliter); } public function reset(){ $this->logger=; }public function save($fileName){ $fp = fopen($fileName,”w”); fwrite($fp,$this->toString(“n”)); fclose($fp); } }class LogStorage{ private $fileName; public function __construct($fileName){ $this->fileName = $fileName; }public function save($text){ $fp = fopen($this->fileName,”w”); fwrite($fp,$text); fclose($fp); }}$logger = new Logger();$logger->add(“First log”);$logger->add(“Second log”);$logger->add(“Third log”);$logStorage = new LogStorage(“pfile.txt”);$logStorage->save($logger->toString(“n”));
So we split our logger class into two different classes one is Logger and other is LogStorage and now each class has only one task or responsibility. Logger class only responsible for collecting logs and LogStorage class responsible for storing logs to file. That makes our code more cohesive.
Open/Close Principle: This principle purpose that existed and well-tested class should not be modified when a new feature needs to be built. It may introduce a new bug when we modify an existing class to make a new feature. So rather than changing an existing class/Interface, we should extend that class/Interface in a new class to add new features. ?Software entities ? should be open for extension, but closed for modification.?
Let?s understand with the example:
<?phpclass SavingAccount{ private $balance; public function setBalance($balance){} public function getBalance(){} public function withdrawal(){}}class FixedDipositAccount(){ private $balance; private $maturityPeriod; public function setBalance($balance){} public function getBalance(){}}class IntrestCalculator{ public function calculate($account) { if ($account instanceof SavingAccount) { return $account->getBalance*3.0; } elseif ($member instanceof FixDipositAccount) { return $account->getBalance*9.5; } throw new Exception(‘Invalid input member’); }}$savingAccount = new SavingAccount();$savingAccount->setBalance(15000);$fdAccount = new FixedDipositAccount();$fdAccount->setBalance(25000);$intrestCalculator = new IntrestCalculator();echo $intrestCalculator->calculate($savingAccount);echo $intrestCalculator->calculate($fdAccount);
In the above example, we have two simple class SavingAccount and FixedDepositAccount and a class IntrestCalculator that calculate interest based on the provided object type. But we have a problem here. Our IntrestCalculator class is not closed for modifications. Whenever we need to add a new type of account we need to change our IntrestCalculator class to support the newly introduced account type in the system. So the above solution violates the Open/Closed principle.Here is the improved implementation:
<?phpinterface Account{ public function calculateInterest();}class SavingAccount implements Account{ private $balance; private $rate=3.0; private $maturityPeriod;public function setBalance($balance){} public function getBalance(){} public function withdrawal(){} public function calculateIntrest(){ $this->$rate*$this->balance; }}class FixedDipositAccount implements Account{ private $balance; private $rate =9.5; public function setBalance($balance){} public function getBalance(){} public function calculateIntrest(){ $this->$rate*$this->balance; }}class IntrestCalculator{ public function calculate(Account $account) { return $account->calculateIntrest(); }}$savingAccount = new SavingAccount();$savingAccount->setBalance(15000);$fdAccount = new FixedDipositAccount();$fdAccount->setBalance(25000);$intrestCalculator = new IntrestCalculator();echo $intrestCalculator->calculate($savingAccount);echo $intrestCalculator->calculate($fdAccount);
Liskov Substitution Principle: This principle is named after the name of Barbara Liskov. She introduced this principle in 1987. The concept states that a subtype must be substitutable to base types without breaking the behavior. It is a particular definition of subtyping relation, called behavioral subtyping.
?Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.?
Let?s understand with a very basic example.
Class A{ public function doSomething(){ }}Class B extends A{ }
In the above example, we have two simple classes A and B, Class B inherit class A. So as per the Liskov Substitution Principle wherever we use class A in our program we should be able to replace it with its subtype class B. Now let?s look at the classical Rectangle Square problem:
<?php// The Rectangle Square problemclass Rectangle{ protected $width; protected $height; public function setHeight($height) { $this->height = $height; } public function getHeight() { return $this->height; } public function setWidth($width) { $this->width = $width; } public function getWidth() { return $this->width; } public function area() { return $this->height * $this->width; }}class Square extends Rectangle{ public function setHeight($value) { $this->width = $value; $this->height = $value; } public function setWidth($value) { $this->width = $value; $this->height = $value; }}class AreaTester{ private $rectangle; public function __construct(Rectangle $rectangle) { $this->rectangle = $rectangle; } public function testArea($width,$height) { $this->rectangle->setHeight($width); $this->rectangle->setWidth($height); return $this->rectangle->area(); }}$rectangle = new Rectangle();$rectangleTest = new AreaTester($rectangle);$rectangleTest->testArea(2,3); // gives 6 as expecated$squre = new Square();$rectangleTest = new AreaTester($squre);$rectangleTest->testArea(2,3); // gives 9 expecated is 6
In the above example, we inherit the Rectangle class to make our Square class and override its methods. At first look, it seems fine but our Square class now no longer substitutable for Rectangle class. The area method will not work as expected for Rectangle if we substitute it with Square. So it violates the LSP.
Without LSP adherence, changes to a class may have unexpected consequences and/or require opening a previously closed class. While following the LSP allows easy extension of program behavior because subclasses can be inserting into working code without causing undesired outcomes.
This principle is just an extension of the Open/Closed Principle and it means that we must make sure that new derived classes are extending the base classes without changing their behavior.
The following are the conditions to avoid LSP violation.
- Method signatures must match and accept equal no of the parameter as the base type
- The return type of the method should match to a base type
- Exception types must match to a base class
Now let?s see an example that adheres LSP:
interface LogRepositoryInterface{ /** * Gets all logs. * * @return array */ public function getAll();}class FileLogRepository implements LogRepositoryInterface{ public function getAll() { // Fetch the logs from the file and return an array return $logsArray; }}class DatabaseLogRepository implements LogRepositoryInterface{ public function getAll() { // fetch Logs from model Log and call toArray() function to match the return type. return Log::all()->toArray();}}
As you can see from the code above, both classes implement the LogRepositoryInterface by having a getAll method. FileRepository read logs from file return array and DatabaseLogRepository reads logs using the Eloquent model all() method that returns a Collection type, So we call toAarry() method on collection to make it the array. If we not call the toArry() method and return a Collection it violates LSP that leads to type checking on client class.
Interface Segregation Principle: This principle states that an interface should not enforce unwanted methods to a class. The idea here is instead of having a large interface we should have smaller interfaces, So we should not create an interface having a lot of methods If we have such an interface we should break in into smaller interfaces.?Many client-specific interfaces are better than one general-purpose interface.?
<?phpinterface IPrintMachine{ public function print(Document $d); public function scan(Document $d); public function xerox(Document $d);}class Document { // some attributes and methods}class AdvancePrinter implements IPrintMachine{ public function print(Document $d){ echo “Print document”; } public function scan(Document $d){ echo “Scan document”; } public function xerox(Document $d){ echo “Take xerox copy of document”; }}class SimplePrinter implements IPrintMachine{ public function print(Document $d){ echo “Print document”; } public function scan(Document $d){ echo “Not supported”; } public function xerox(Document $d){ echo “Not supported”; }}<?phpinterface IPrinter{ public function print(Document $d);}interface IScanner{ public function scan(Document $d);}interface IXerox{ public function xerox(Document $d);}class Document { // some attributes and methods}class AdvancePrinter implements IPrinter,IScanner,IXerox{ public function print(Document $d){ echo “Print document”; } public function scan(Document $d){ echo “Sacn document”; } public function xerox(Document $d){ echo “Take xerox copy of document”; }}class SimplePrinter implements IPrinter public function print(Document $d){ echo “Print document”; }}
Dependency Inversion Principle: According to this principle higher level classes should not directly depend on lower level classes but abstractions. It means that a higher level class should not need to know the implementation details of the low-level class, the low-level class should be hidden behind an abstraction.Here is an example: let?s suppose we have a Post class that represents a blog post and read/write posts from db, To do that it requires a database connection. So our first attempt may look like this.
<?phpclass MySqlConnection { public function connect() {}} class Post{ private $dbConnection; public function __construct(MySqlConnection $dbConnection) { $this->dbConnection = $dbConnection; $this->dbConnection->connect(); }}
In the above implementation, we have defined a MySqlConnection class that has a connect method to make a connection to db and we pass this MySqlConnection class to the constructor of Post class. The problem here is that our post-class depends on a concrete class MySqlConnection. Now in the future, if we need to support another DB we have to change our Post class.To avoid this we have to refactor our Post class and instead of passing a concrete class type in the constructor we pass a DbConnection interface that is implemented by MySqlConnection class.
interface DbConnectionInterface { public function connect();} class MySqlConnection implements DbConnectionInterface { public function connect() {}} class Post { private $dbConnection; public function __construct(DbConnectionInterface $dbConnection) { $this->dbConnection = $dbConnection; $this->dbConnection->connect(); }}
So now there is no need to change Post class for new DB type. The only thing that we need to support the new DB is that it must implement the interface DBConnectionInterface.