Skip to content

Instantly share code, notes, and snippets.

@nspalo
Last active May 5, 2023 21:20
Show Gist options
  • Save nspalo/9f00a1bc819e1efb688372e3e9b1316c to your computer and use it in GitHub Desktop.
Save nspalo/9f00a1bc819e1efb688372e3e9b1316c to your computer and use it in GitHub Desktop.
SOLID Principles

SOLID PRINCIPLE

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.

Single Responsibility Principle (SRP)

A class should have One and Only responsibility.

  • Should only be responsible for a single task
  • Should only depend on one part of the business

Incorrect code

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();
});

Correct code

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);
});

Open and Close Principle (OCP)

Objects or Entity should be open for extension but close to modification

  • 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

Incorrect code

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.

Correct code

When you have a class, or a method that you want to extend without modifying it

  • 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);
  }
}

Liskov's Substitution Principle (LSP)

Parent Classes and Sub-Classes area substitutable to each other

  • 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)

5 Rules

  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 then parent function pre-conditions
  4. Child function post-conditions cannot be lesser then the 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

Rule 1 & 2, arguments and return types

  1. Child function arguments must match function arguments of parent
  2. 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

Interface Segregation Principle (ISP)

  • 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
  }
}

Dependency Inversion Principle (DIP)

  • 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.

  1. What are higher level module(s)?
  • Anything that accepts the abstractions and does something with it
  1. What are lower level module(s)?
  • Any class implementing the abstractions
  1. 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();
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment