Clean, Reusable, Maintainable, Testable, and Extendable Code
- S Single Responsibility Principle
- O Open-Close Principle
- L Liskov's Substitution Principle
- I Interface Segregation Principle
- D Dependency Inversion Principle
What is SOLID?
In object-oriented computer programming, SOLID is a mnemonic acronym for five design principles
intended to make software designs more understandable, flexible, and maintainable.
The principles are a subset of many principles promoted by American software engineer and instructor
Robert C. Martin a.k.a. Uncle Bob.
- Should only be responsible for a single task
- Should only depend on one part of the business
class User
{
public $name;
public $email;
public function __construct($data)
{
$this->name = $data['name'];
$this->email = $data['email'];
}
public function formatJson()
{
return json_encode([
'name' => $this->name,
'email' => $this->email,
]);
}
public function validate($data)
{
if(!isset($data['name'])) {
throw new \RuntimeException('Some message');
}
if(!isset($data['email'])) {
throw new \RuntimeException('Some message');
}
}
}
Route::get('/', function(){
$data = request()->query();
$user = new User($data);
$user->validate($data);
return $user->formatJson();
});
class Json
{
public static function from($data)
{
return json_encode($data);
}
}
class UserRequestValidator
{
public static function validate($data)
{
foreach($data as $property => $type) {
if(typeOf($data[$property]) !== $type) {
throw new \RuntimeException('Some message');
}
}
}
}
class User
{
public $name;
public $email;
public function __construct($data)
{
$this->name = $data['name'];
$this->email = $data['email'];
}
}
Route::get('/', function(){
$data = request()->query();
UserRequestValidator::validate($data);
$user = new User($data);
return Json::from($user);
});
- When you have a class, or a method you want to extend without modifying it
- separate the extensible behavior behind an interface,
- then flip the dependencies
~ Uncle Bob
Your boss wanted a feature, to calculate the area of a Square.
class Square
{
public $height;
public $width;
}
class AreaCalculator
{
public function calculate($squares)
{
$area = [];
foreach($squares as $square) {
$area[] = $square->width * $square->height;
}
return array_sum($area);
}
}
then your boss wanted to add a new feature, and that is to be able to calculate the area of a Circle...
class Square
{
public $height;
public $width;
}
class Circle
{
public $radius;
}
class AreaCalculator
{
public function calculate($shapes)
{
$area = [];
foreach($shapes as $shape)
{
if(is_a($shape, 'Square')) {
$area[] = $shape->width * $shape->height;
}
else if (is_a($shape, 'Circle')) {
$area[] = pi() * ($shape->radius * $shape->radius);
}
}
return array_sum($area);
}
}
After a few months your boss wanted to add a new feature,
this time it's the ability to calculate the area of a Triangle,
so you modify the code as shown below.
class Square
{
public $height;
public $width;
}
class Circle
{
public $radius;
}
class Triangle
{
public $base;
public $height;
}
class AreaCalculator
{
public function calculate($shapes)
{
$area = [];
foreach($shapes as $shape) {
if(is_a($shape, 'Square')) {
$area[] = $shape->width * $shape->height;
}
else if (is_a($shape, 'Circle')) {
$area[] = pi() * ($shape->radius * $shape->radius);
}
else if (is_a($shape, 'Triangle')) {
$area[] = ($shape->base * $shape->height) / 2;
}
}
return array_sum($area);
}
}
The code above may did the job done, but it is not good as it violates the Open-Close Principle whereas you modify the class in order to extend the functionality.
- Separate the extensible behavior behind an interface
- Flip the dependencies.
interface Shape
{
public function area(); // This is the extensible behavior
}
class Square implements Shape
{
public $height;
public $width;
public function area()
{
return $this->width * $this->height;
}
}
class Circle implements Shape
{
public $radius;
public function area()
{
return pi() * ($this->radius * $this->radius);
}
}
class Triangle implements Shape
{
public $base;
public $height;
public function area()
{
return ($this->base * $this->height) / 2;
}
}
class AreaCalculator
{
public function calculate(Shape $shapes)
{
$area = [];
foreach($shapes as $shape) {
$area[] = $shape->area();
}
return array_sum($area);
}
}
- Strong Behavioral Sub-typing
- Behavior
- Is defined by class methods
- Sub-type
- Is the acquired behavior from the parent class
- Strong
- Stricter rules(than normal types)
- Sub-types MUST abide the rules
- Stronger rules apply to children (When overriding a parent functionality)
- Child function arguments must match function arguments of parent
- Child function return type must match parent function return type
- Child pre-conditions cannot be greater then parent function pre-conditions
- Child function post-conditions cannot be lesser then the parent function post-conditions
- Exceptions thrown by child method must be the same as or inherit from an exception thrown by the parent method
Rule 1 & 2, arguments and return types
- Child function arguments must match function arguments of parent
- Child function return type must match parent function return type
class ParentClass
{
public $id;
public function setId(int $id): void
{
$this->id = $id;
}
}
class ChildClass extends ParentClass {
public function setId(int $id): void
{
$this->id = $id;
}
}
Rule 3 & 4, pre-conditions and post-conditions
- First, lets explain/take a look at what is a pre and post conditions
function addFive($number)
{
// pre-condition
// A pre-condition is a conditional that happens before executing a functionality
if(!is_int($number)) {
throw new \RuntimeException('Some message');
}
// functionality
$total = $number + 5;
// post-condition
// A post-condition is a conditional that happens after executing a functionality
if(!is_int($total)) {
throw new \RuntimeException('Some message');
}
return $total;
}
Refactoring the code...
// pre-condition, will return an error if it's not an integer
function addFive(int $number)
{
$total = $number + 5;
// post-condition
// A post-condition is a conditional that happens after executing a functionality
if(!is_int($total)) {
throw new \RuntimeException('Some message');
}
return $total;
}
// post-condition, will return an error if return type is not an integer
function addFive(int $number): int
{
$total = $number + 5;
return $total;
}
// Final result
function addFive(int $number): int
{
return $number + 5;
}
Rule 3, child and parent class pre-conditions
3. Child pre-conditions cannot be greater than parent function pre-conditions
class File
{
public function parse($file)
{
// parse file
}
}
class JsonFile extends File
{
public function parse($file)
{
/**
* This breaks Liskov Substitution Principle
* - we added a pre-condition that the parent class does not have
* -- the pre-condition of the child method is greater than the parent method
* - we are not gonna be able to substitute the JsonFile in place of File
*/
if(pathinfo($file, PATHINFO_EXTENSION) !== 'json') {
throw new \RuntimeException('Some message');
}
// parse file
}
}
How do we fix it?
interface File
{
public function __construct(string $file);
public function parse();
}
class JsonFile implements File
{
public $file;
public function __construct(string $file)
{
$this->file = $file;
}
public function parse()
{
// parse json from $this->file
}
}
class HtmlFile implements File
{
public $file;
public function __construct(string $file)
{
$this->file = $file;
}
public function parse()
{
// parse html from $this->file
}
}
function readFrom(File $file)
{
$file->parse();
}
Rule 4, child and parent class post-conditions 4. Child function post-conditions cannot be lesser then the parent function post-conditions
// This is how we violate it
interface File
{
public function __construct(string $file);
public function parse();
}
class JsonFile implements File
{
public $file;
public function __construct(string $file)
{
$this->file = $file;
}
public function parse($content)
{
// parse json using this $this->file
$content = '{}'; // parsed json content
return json_decode($content);
}
}
class HtmlFile implements File
{
public $file;
public function __construct(string $file)
{
$this->file = $file;
}
public function parse($content)
{
// return boolean if theres content
if($content) {
return true;
}
else {
return false;
}
}
}
function readFrom(File $file)
{
$content = $file->parse();
if(is_array($content))
{
// do this
}
else if(is_bool($content))
{
// do that
}
}
Let's Fix it...
interface File
{
public function __construct(string $file);
public function parse(): void;
}
class JsonFile implements File
{
public $file;
public function __construct(string $file)
{
$this->file = $file;
}
public function parse(): void
{
// parse here
}
}
class HtmlFile implements File
{
public $file;
public function __construct(string $file)
{
$this->file = $file;
}
public function parse(): void
{
// parse here
}
}
function readFrom(File $file)
{
$file->parse();
}
Rule 5, child and parent class exceptions 5. Exceptions thrown by child method must be the same as or inherit from an exception thrown by the parent method
- No client should be forced to depend on method is doesn't use
- No class should be forced to implement an interface that have a method it will never use
/**
* interface are contracts that force classes
* to depend a given method or group of method
*/
interface File
{
public function parse();
}
// client = class
class JsonFile implements File
{
public function parse()
{
// parse here
}
}
We violate it when a class is forced to implement function that it doesn't need/use
/**
* Interface = Contract
*/
interface File
{
public function parse();
public function htmlContent();
}
/**
* client = class
* - This class is forced to implement the method htmlContent
*/
class JsonFile implements File
{
public function parse()
{
// parse here
}
public function htmlContent()
{
// Not needed behavior
}
}
To fix it, we need to segregate the behavior
/**
* Interface = Contract
* Segregation = Divide or Split
*/
interface File
{
public function parse();
}
interface Html
{
public function htmlContent();
}
/**
* client = class
* - Now this class is not force to implement the htmlContent method
*/
class JsonFile implements File
{
public function parse()
{
// parse here
}
}
- High level modules should not depend on low level modules;
both should depend on abstractions.- Abstractions should not depend on details.
- Details should depend upon abstractions.
- What are higher level module(s)?
- Anything that accepts the abstractions and does something with it
- What are lower level module(s)?
- Any class implementing the abstractions
- What are the abstractions and how do we depend on them?
- Interface
// Abstractions
interface ToiletInterface
{
public function flush();
}
// Lower level module
class PortaPottyToilet implements ToiletInterface
{
public function flush()
{
}
}
class GoldenToilet implements ToiletInterface
{
public function flush()
{
}
}
// Higher level module
class Human
{
public function donePooping(ToiletInterface $toilet)
{
$toilet->flush();
}
}