Skip to content

Instantly share code, notes, and snippets.

@AurelienPillevesse
Last active October 27, 2022 11:51
Show Gist options
  • Select an option

  • Save AurelienPillevesse/cba1a4b60c14ed5b64b1bb58e8b60a8e to your computer and use it in GitHub Desktop.

Select an option

Save AurelienPillevesse/cba1a4b60c14ed5b64b1bb58e8b60a8e to your computer and use it in GitHub Desktop.
SOLID

Responsabilité unique - Single Responsibility Principle

Une classe, une fonction ou une méthode doit avoir une et une seule responsabilité.

A class should have only a single responsibility (i.e only one potential change in the software's specification should be able to affect the specification of the class.

Violating the Single Responsibility Principle

class Document {
  protected $title;
  protected $content;
  protected $pdf;

  public function construct($title, $content) {
    $this->title = $title;
    $this->content = $content;
  }

  public function generatePDF() {
    return "Output the document as pdf: " . $this->title . $this->content;
  }
}
//instantiate
$document = new Document('Hello', 'Hello World!');
$document->generatePDF();

The generatePDF method violates the Single Responsibility Principle because if we wanted to change how the PDF is produced we would need to change the class.

Complying with the Single Responsibility Principle

class Document {
  protected $title;
  protected $content;

  public function __construct($title, $content) {
    $this->title = $title;
    $this->content = $content;
  }
}

class Pdf {
  protected $pdf;

  public function __construct() {}

  public function generate(Document $document) {
    return "Output the document as pdf: " . $this->title . $this->content;
  }
}

//instantiate
$document = new Document('Hello', 'Hello World!');
$pdf = new Pdf();
$pdf->generate($document);

In the above example the Document class only knows about the data it handles. The PDF class is responsible for generating the document as a PDF. If we wanted to output the document as HTML we would just create a new class class HTML with a generate method that return the document title and content as html content.

Ouvert/Fermé - Open Closed Principle

Une entité applicative (classe, fonction, module ...) doit être fermée à la modification directe mais ouverte à l'extension.

A software module (class or method) should be open for extension but closed for modification

Violating the Open Closed Principle

class Rectangle {
	public $height;
	public $width;

	public function __construct($height, $width = $height) {
		$this->height = $height;
		$this->width = $width;
	}
}

class Triangle {
	public $height;
	public $base;

	public function __construct($height, $base) {
		$this->height = $height;
		$this->base = $base;
	}
}

class Circle {
	public $radius;

	public function __construct($radius) {
		$this->radius = $radius;
	}
}

class Area {
	public function calculate($shape) {
		if(is_a($shape, Rectangle::class) || is_a($shape, Square::class) {
			return $this->height * $this->width;
		} else if(is_a($shape, Triangle::class)) {
			return ($this->height * $this->base) / 2;
		} else if(is_a($shape, Circle::class)) {
			return $this->radius * $this->radius * pi();
		}
	}
}

//now output the areas
class ShapeController {
	public function outputArea(Area $area) {
		//instantiate
		$square = new Rectangle(10);
		$rectangle = new Rectangle(10, 20);
		$triangle = new Triangle(10, 5);
		$circle = new Circle(5);

		return new Response(
		    [
		    	'square' => $area->calculate($square),
			'rectangle' => $area->calculate($rectangle),
			'triangle' => $area->calculate($triangle),
			'circle' => $area->calculate($circle),
		    ]
		);
	}
}

The problem with the above method, if we want to add a new shape the Area Calculator class would need changing and a new shape class added. This is because the area calculator is responsible for calculating the areas of all the different shapes.

Following the open closed principle, the area of each shape should be calculated by the class responsible for that shape.

Complying with the Open Closed Principle

interface Shapeable {
	public function area();
}

class Rectangle implements Shapeable {
	public $height;
	public $width;

	public function __construct($height, $width = $height) {
		$this->height = $height;
		$this->width = $width;
	}

	public function area() {
		return $this->height * $this->width;
	}
}

class Triangle implements Shapeable {
	public $height;
	public $base;

	public function __construct($height, $base) {
		$this->height = $height;
		$this->base = $base;
	}

	public function area() {
		return ($this->height * $this->base) / 2;
	}
}

class Circle implements Shapeable {
	public $radius;

	public function __construct($radius) {
		$this->radius = $radius;
	}

	public function area() {
		return $this->radius * $this->radius * pi();
	}
}

class Area {
	public function calculate(Shapeable $shapeable) {
		return $shapeable->area();
	}
}

//now output the areas
class ShapeController {
	public function outputArea(Area $area) {
		//instantiate
		$square = new Rectangle(10);
		$rectangle = new Rectangle(10, 20);
		$triangle = new Triangle(10, 5);
		$circle = new Circle(5);

		return new Response(
		    [
		    	'square' => $area->calculate($square),
			'rectangle' => $area->calculate($rectangle),
			'triangle' => $area->calculate($triangle),
			'circle' => $area->calculate($circle),
		    ]
		);
	}
}

With the method above when the area of a new shape needs calculating a new class with an area method needs adding and nothing else. If we wanted to calculate the area of a regular pentagon, we could add:

class Pentagon implements Shapeable {
	public $side;
	public $apothem;

	public function __construct($side, $apothem) {
		$this->side = $side;
		$this->apothem = $apothem;
	}

	public function area() {
		$triangle= new Triangle($this->apothem, $this->side);
		return $triangle * 5;
	}
}

$pentagon = new Pentagon(3, 2);
$areaCalculator->calculate($pentagon);

Substitution de Liskov - Liskov Substitution Principle

Une instance de type T doit pouvoir être remplacée par une instance de type G, tel que G sous-type de T, sans que cela ne modifie la cohérence du programme.

The principle defines that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. That requires the objects of your subclasses to behave in the same way as the objects of your superclass.

In subtypes, method arguments cannot be made narrower and method return types cannot be made wider.

Violating the Liskov Substitution Principle

interface BankAccount {
	public function withdraw(float $amount);
	public function deposit(float $amount):float;
}

class SavingAccount implements BankAccount {
	public function withdraw(float $amount):bool {
		if($this->balance - $amount >= 1000) {
			return true;
		} else {
			return false;
		}
	}
	public function deposit(float $amount): float {
		return $this->balance + $amount;
	}
}

class CurrentAccount implements BankAccount {
	public function withdrawal(float $amount):bool {
		if($this->balance - $amount > 0) {
			return true;
		} else {
			return false;
		}
	}
	public function deposit(float $amount):float {
		return $this->balance + $amount;
	}
}

class PackageAccount extends CurrentAccount {
	protected $monthlyFee;
	public function withdraw(float $amount):bool {
		if($this->balance - $amount - $this->monthlyFee > 0) {
			return true;
		} else {
			return false;
		}
	}
	public function deposit(float $amount):float {
		return $this->balance + $amount;
	}
}

class StudentAccount extends CurrentAccount {
	protected $overdraft;
	public function withdraw(float $amount):bool {
		if(($this->balance + $this->overdraft) - $amount > 0) {
			return true;
		} else {
			return false;
		}
	}
	public function deposit(float $amount):float {
		return $this->balance + $amount;
	}
}

class FixDepositSavingAccount implements BankAccount {
	public function withdraw(float $amount):bool {
		throw new Exception(“Not supported by this account type”);
	}
	public function deposit(float $amount):float {
		if($balanceAfterWithdrawal < 100) {
			throw new Exception(“Not supported by this account type”);
		} else {
			return $this->balance + $amount;
		}
	}
}

class AccountManager {
	public function withdrawal(Account $account):void {
	  $account->withdraw(10.00);
	}
	public function upgradeAccount(CurrentAccount $account):PackageAccount {

	}
}

class StudentAccountManager extends AccountManager {
	public function upgradeAccount(StudentAccount $account):CurrentAccount {

	}
}

The upgradeAccount method in the StudentAccountManager class violates the Liskov Substitution principle because the argument has been narrowed from CurrentAccount to StudentAccount. The return type has also been widened from Package Account to CurrentAccount. Furthermore as exception is thrown when FixDepositSavingAccount account is passed to the withdrawal method of AccountManager. This could be fixed by amending the withdrawl method to check the account type but this would break the open closed principle:

class AccountManager {
	public function withdrawal(Account $account):void {
		if(!is_a($account, FixDepositSavingAccount) {
			$account->withdraw(10.00);
		}
	}
	public function upgradeAccount(CurrentAccount $account):PackageAccount {

	}
}

Complying with the Liskov Substitution Principle

interface BankAccount {
	public function deposit(float $amount):float;
}

class WithdrawalSavingAccount implements BankAccount {
	public function withdraw(float $amount);
}

class NonWithdrawalSavingAccount implements BankAccount {

}

class SavingAccount implements WithdrawalSavingAccount {
	public function withdraw(float $amount):bool {
		if($this->balance - $amount >= 1000) {
			return true;
		} else {
			return false;
		}
	}
	public function deposit(float $amount): float {
		return $this->balance + $amount;
	}
}

class CurrentAccount implements WithdrawalSavingAccount {
	public function withdrawal(float $amount):bool {
		if($this->balance - $amount > 0) {
			return true;
		} else {
			return false;
		}
	}
	public function deposit(float $amount):float {
		return $this->balance + $amount;
	}
	public function upgrade() {
		return new PackageAccount();
	}
}

class PackageAccount extends CurrentAccount {
	protected $monthlyFee;
	public function withdraw(float $amount):bool {
		if($this->balance - $amount - $this->monthlyFee > 0) {
			return true;
		} else {
			return false;
		}
	}
	public function deposit(float $amount):float {
		return $this->balance + $amount;
	}
}

class StudentAccount extends CurrentAccount {
	protected $overdraft;
	public function withdraw(float $amount):bool {
		if(($this->balance + $this->overdraft) - $amount > 0) {
			return true;
		} else {
			return false;
		}
	}
	public function deposit(float $amount):float {
		return $this->balance + $amount;
	}
	public function upgrade() {
		return new CurrentAccount();
	}
}

class FixDepositSavingAccount implements NonWithdrawalSavingAccount {
	public function withdraw(float $amount):bool {
		throw new Exception(“Not supported by this account type”);
	}
	public function deposit(float $amount):float {
		if($balanceAfterWithdrawal < 100) {
			throw new Exception(“Not supported by this account type”);
		} else {
			return $this->balance + $amount;
		}
	}
}

class AccountManager {
	public function withdrawal(WithdrawalSavingAccount $account):void {
		$account->withdraw(10.00);
	}
	public function upgradeAccount(Account $account): Account {
		return $account->upgrade();
	}
}

class StudentAccountManager {
	public function upgradeStudentAccount(Account $account): Account {
		return $account->upgrade();
	}
}

Now the withdrawal method of AccountManager will only make withdrawals from withdrawal savings accounts that have the withdrawal method. The upgrade account is taken care of by the account class rather than the manager so the upgrade method on the account just needs to be called.

Ségragation d'interface - Interface Segregation Principle

Préférer plusieurs interfaces spécifiques pour chaque client plutôt qu'une seule interface générale.

A client should never be forced to depend on methods it doesn't use. Altering one method in a class shouldn't affect classes that don't depend on it. Replace fat interfaces with many small, specific interfaces

Violating the Interface Segregation Principle

interface Workable {
    public function sleep();
    public function eat();
    public function work();
}

class Human implements Workable {
    public function sleep() {
        return true;
    }
    public function eat() {
        return true;
    }
    public function work() {
        return true;
     }
}

class Android implements Workable {
    public function sleep() {
        return false;
    }
    public function eat() {
        return false;
    }
    public function work() {
        return true;
     }
}

This violates the Interface Segregation Principle because Android does not need to sleep or eat only work.

Complying with the Interface Segregation Principle

interface Sleepable {
   public function sleep();
}
interface Eatable {
  public function eat();
}
interface Workable {
   public function work();
}

class Human implements Workable, Eatable, Sleepable {
    public function sleep() {
        return true;
    }
    public function eat() {
        return true;
    }
    public function work() {
        return true;
    }
}

class Android implements Workable {
    public function work() {
        return true;
    }
}

Inversion de dépendance - Dependency Inversion Principle

Il faut dépendre des abstractions, pas des implémentations.

High level modules should not depend on low level modules Abstractions should not depend on details. Details should depend upon abstactions

Violating the Dependency Inversion Principle

class Article {

    public function all() {
        return 'Returning all articles from local database';
    }
}
class Display {
    protected $artical;
    public function __construct(Artical $artical) {
        $this->artical = $artical;
    }

    public function articles() {
        return $this->artical->all();
    }
}

The Display class is tightly coupled to the Article class used to retrieve the articles. If the data source for the articles changed the Display class would also have to be changed.

Complying with the Dependency Inversion Principle

interface ArticleInterface {
    public function all();
}

class MysqlArticle implements ArticleInterface {
    public function all() {
        return 'Returning all articles from local database';
    }
}

class ApiArticle implements ArticleInterface {
    public function all() {
        return 'Returning all articles from api';
    }
}

class Display {
    protected $article;
    public function __construct(ArticleInterface $article) {
        $this->article = $article;
    }
    public function articles() {
        return $this->article->all();
    }
}

By coupling the Display class to the ArticleInterface it easy to change the datasource of the articles from the database to the api.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment