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.
Wikipedia
Generally, I don’t want to show a deep analysis of each principle. It’s just a review in simple words which in my opinion must have to know for OOP languages as for example PHP.
The target of the SOLID principles
The SOLID principles are a set of rules for OOP that are recommended to be applied while working on software for better maintainability and scalable of your product.
The rules aren’t simple and vague, and all developers understand them differently. With a new experience, developers have a new understanding of how these rules can be applied. The most important thing is to understand, learn, and start using them, and rethinking these rules and improving your skills will come later in the course of work.
Surprisingly, the principles were formulated several decades ago and are still relevant today. This can only speak of their effectiveness.
The Single Responsibility Principle
Each software module should have one and only one reason to change
Wikipedia
In the classical explanation by Robert Martin, in short, the main reason for changes its people. You need to think about a person who needed this feature or who can ask about changes or influences it.
But many sources talk about function/class size, but this is not the case. You can have a function/class the size of Saturn and it will follow this principle. Let’s see it for example.
Example
We’ve got an Order
class:
<?php
class Order {
public function calculate_total_sum() {/*...*/}
public function get_items() {/*...*/}
public function get_item_count() {/*...*/}
public function add_item( $item ) {/*...*/}
public function delete_item( $item ) {/*...*/}
public function print_order() {/*...*/}
public function show_order() {/*...*/}
public function load() {/*...*/}
public function save() {/*...*/}
public function update() {/*...*/}
public function delete() {/*...*/}
}
We can separate the logic of the Order
to an entity, part for work with a database, and part for different views.
<?php
class Order {
public function calculate_total_sum() {/*...*/}
public function get_items() {/*...*/}
public function get_item_count() {/*...*/}
public function add_item( $item ) {/*...*/}
public function delete_item( $item ) {/*...*/}
}
class OrderRepository {
public function load( $orderID ) {/*...*/}
public function save( $order ) {/*...*/}
public function update( $order ) {/*...*/}
public function delete( $order ) {/*...*/}
}
class OrderViewer {
public function print_order( $order ) {/*...*/}
public function show_order( $order ) {/*...*/}
}
It means if something happens with a database this is a reason for changing the OrderRepository
class, with a view – OrderViewer
, with the entity – Order
.
The Open-Closed Principle
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
Wikipedia
In a perfect world, it would be necessary to add new code to add new functionality but not change the old.
Bug fixing, refactoring, and performance improvements don’t violate this principle. The principle says exactly about changing the business logic of the software.
Example
Method load
of the class OrderRepository
described the logic for creating the order:
<?php
class OrderRepository {
public function load( $orderID ) {
$pdo = new PDO(
$this->config->getDsn(),
$this->config->getDBUser(),
$this->config->getDBPassword()
);
$statement = $pdo->prepare( "SELECT * FROM `orders` WHERE id=:id" );
$statement->execute( array( ":id" => $orderID ) );
return $query->fetchObject( "Order" );
}
public function save( $order ) {/*...*/}
public function update( $order ) {/*...*/}
public function delete( $order ) {/*...*/}
}
Then the business wants to create the orders by the 3rd party API. What do we need to do?
- Create the interface
IOrderSource
- Create 2 different classes
MySQLOrderSource
andApiOrderSource
which implement the current interface - Pass to the constructor of
OrderRepository
class the object which implements theIOrderSource
inteface.
<?php
interface IOrderSource {
public function load( $orderID );
public function save( $order );
public function update( $order );
public function delete( $order );
}
class MySQLOrderSource implements IOrderSource {
public function load( $orderID ) {/*...*/}
public function save( $order ) {/*...*/}
public function update( $order ) {/*...*/}
public function delete( $order ) {/*...*/}
}
class ApiOrderSource implements IOrderSource {
public function load( $orderID ) {/*...*/}
public function save( $order ) {/*...*/}
public function update( $order ) {/*...*/}
public function delete( $order ) {/*...*/}
}
class OrderRepository {
private $source;
public function __constructor( IOrderSource $source ) {
$this->source = $source;
}
public function load( $orderID ) {
return $this->source->load( $orderID );
}
public function save( $order ) {/*...*/}
public function update( $order ) {/*...*/}
}
The Liskov Substitution Principle
If S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, a task performed, etc.)
Wikipedia
Sounds good are you agree? In simple terms, the extended class must have the same output format such as parent methods.
Example
We’ve got a LessonRepository
class that returns an array of all lessons from a file in the get_all
method. There was a need to get lessons from the database. Create the DatabaseLessonRepository
class, inherit from LessonRepository, and rewrite the get_all
method.
<?php
class LessonRepository {
public function get_all() {
return $files; //return array of lesson through file system.
}
}
class DatabaseLessonRepository extends LessonRepository {
public function get_all() {
return Lesson::all(); //return a Collection type instead of array
}
}
In the get_all
method of the DatabaseLessonRepository
class, instead of a collection, we must return an array. For example, we can use the type hinting:
<?php
interface LessonRepositoryInterface {
public function get_all(): array;
}
class FilesystemLessonRepository implements LessonRepositoryInterface {
public function get_all(): array {
return $files;
}
}
class DatabaseLessonRepository implements LessonRepositoryInterface {
public function get_all(): array {
return Lesson::all()->toArray();
}
}
The Interface Segregation Principle
Split interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.
Wikipedia
Example
Really hard to find an example that can easily describe this principle without diving into context. I want to show the main trigger of this principle that in my opinion will be the best explanation.
<?php
interface IInterface {
public function method1();
public function method2();
}
class SomeClass1 implements IInterface {
public function method1() { /*...*/ }
public function method2() { /*...*/ }
}
When the business logic grew and we’ve been adding yet one class. But unfortunately, to create the implementation of method2
we can’t for any reason.
<?php
class SomeClass2 implements IInterface {
public function method1() { /*...*/ }
public function method2() {
throw new Exception( 'I can\'t implement this method' );
}
}
The right way decompose the interface into a few smaller interfaces
<?php
interface IInterface1 {
public function method1();
}
interface IInterface2 {
public function method2();
}
class SomeClass1 implements IInterface1, IInterface2 {
public function method1() { /*...*/ }
public function method2() { /*...*/ }
}
class SomeClass2 implements IInterface1 {
public function method1() { /*...*/ }
}
Generally, to decide this problem need a lot of code movements and refactor the code. That’s means you need to get down to interface description at the project design stage and make it serious attention to scalable and maintenance.
The Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces). Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
Wikipedia
The best way to start to use this principle is the dependency injection pattern. If you don’t understand this pattern then just immediately diving into it. This can help you be promoted to the next higher development level.
Example
We’ve got an EBookReader
class with the read method. When it becomes necessary to read a class other than just a PDF file, an observer is needed.
<?php
class EBookReader {
public function read() {
$pdf_book = new PDFBook();
return $pdf_book->read();
}
}
class PDFBook {
public function read() { /*...*/}
}
Let’s have resolved the first part of this principle. So who is who? The EBookReader
class is a high-level module that depends on PDFBook
a low-level module. What we can do? Use the dependency injection pattern:
<?php
class EBookReader {
private $book;
public function __construct( PDFBook $book ) {
$this->book = $book;
}
public function read() {
return $this->book->read();
}
}
We haven’t got abstraction but only the detail is PDFBook
object. Just need to create an abstraction for a more flexible solution.
<?php
interface EBook {
public function read();
}
class EBookReader {
private $book;
public function __construct( EBook $book ) {
$this->book = $book;
}
public function read() {
return $this->book->read();
}
}
class PDFBook implements EBook {
public function read() {/*...*/}
}
class MobiBook implements EBook {
public function read() {/*...*/}
}
Conclusion
The SOLID principles are crucial to use and catch. You need to get to know them as early as possible and use them in your work. As you gain experience, these principles will become clearer to you.