What is SOLID? 🙄
It is a set of principles to have good software design practices compiled by Uncle Bob.
Why should I use them?
- Software design principles or conventions.
- Widely accepted in the industry.
- Help make code more maintainable and tolerant to changes.
- Applicable in terms of class design (micro-design), and also at the software architecture level.
If you don't use SOLID, you probably code STUPID¹ without knowing it
¹: STUPID stands for: Singleton, Tight Coupling, Untestability, Premature Optimization, Indescriptive Naming, Duplication
What does SOLID stands for
SOLID is an acronym that stands for:
- Single responsibility principle
- Open/closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
What do we do
S - Single Responsibility Principle(SRP)
💡 A class should only have one reason to change, which means it should only have one responsibility.
- How to accomplish
- Small classes with limited objectives
- Purpose or gain:
- High cohesion and robustness
- Allow class composition (inject collaborators)
- Avoid code duplication
Example
Let's imagine we have a class that represents a text document, said document has a title and content. This document must be able to be exported to HTML and PDF.
Violation of the SRP 👎
⚠️ Code coupled with more than one responsibility
class Document
{
protected $title;
protected $content;
public function __construct(string $title, string $content)
{
$this->title = $title;
$this->content= $content;
}
public function getTitle(): string
{
return $this->title;
}
public function getContent(): string
{
return $this->content;
}
public function exportHtml() {
echo "DOCUMENT EXPORTED TO HTML".PHP_EOL;
echo "Title: ".$this->getTitle().PHP_EOL;
echo "Content: ".$this->getContent().PHP_EOL.PHP_EOL;
}
public function exportPdf() {
echo "DOCUMENT EXPORTED TO PDF".PHP_EOL;
echo "Title: ".$this->getTitle().PHP_EOL;
echo "Content: ".$this->getContent().PHP_EOL.PHP_EOL;
}
}
As you may see the methods or functions that we expose as APIs for other programmers to use, include the getTitle()
and the getContent()
but these methods are being used within the behavior of the same class.
This breaks the Tell-Don't-Ask principle:
💬 Tell-Don't-Ask is a principle that helps people remember that object-orientation is about bundling data with the functions that operate on that data. It reminds us that rather than asking an object for data and acting on that data, we should instead tell an object what to do.
Finally, we also see that the class that must represent a document not only has the responsibility of representing it, but also of exporting it in different formats.
Following the SRP Principle 👍
Once we have identified that the Document
class should not have anything other than the representation of a "document", the next thing we have to establish is the API through which we want to communicate with the export.
For the export we will need to create an interface
that receives a document.
interface ExportableDocumentInterface
{
public function export(Document $document);
}
The next thing we have to do is extract the logic that does not belong to the class.
class HtmlExportableDocument implements ExportableDocumentInterface
{
public function export(Document $document)
{
echo "DOCUMENT EXPORTED TO HTML".PHP_EOL;
echo "Title: ".$document->getTitle().PHP_EOL;
echo "Content: ".$document->getContent().PHP_EOL.PHP_EOL;
}
}
class PdfExportableDocument implements ExportableDocumentInterface
{
public function export(Document $document)
{
echo "DOCUMENT EXPORTED TO PDF".PHP_EOL;
echo "Title: ".$document->getTitle().PHP_EOL;
echo "Content: ".$document->getContent().PHP_EOL.PHP_EOL;
}
}
Leaving the class implementation something like this
class Document
{
protected $title;
protected $content;
public function __construct(string $title, string $content)
{
$this->title = $title;
$this->content= $content;
}
public function getTitle(): string
{
return $this->title;
}
public function getContent(): string
{
return $this->content;
}
}
This makes it easier for both the exports and the documentation class to be better tested.
O - Open-Closed Principle(OCP)
💡 Objects or entities should be open for extension, but closed for modification.
- How to accomplish
- Avoiding depending on specific implementations, making use of abstract classes or interfaces.
- Purpose or gain:
- Makes it easy to add new use cases to our application
Examples
Let's imagine that we need to implement a login system. Initially to authenticate our user we need a username and a password (Main use case), so far so good, but what happens if we require or from business require that the user authenticate through Twitter or Gmail?
To begin with, if a situation like this arises, it is important to understand that what is being asked of us is a new feature and not that we modify the current one. And that the case of Twitter would be one use case and that of Gmail another totally different.
Third Party API Login - OCP Violation 👎
class LoginService
{
public function login($user)
{
if ($user instanceof User) {
$this->authenticateUser($user);
} else if ($user instanceOf ThirdPartyUser) {
$this->authenticateThirdPartyUser($user);
}
}
}
Login with third party API - Following OCP 👍
The first thing we should do is create an interface that complies with what we want to do and that fits the specific use case.
interface LoginInterface
{
public function authenticateUser(UserInterface $user);
}
Now we should decouple the logic that we had already created for our use case and implement it in a class that implements our interface.
class UserAuthentication implements LoginInterface
{
public function authenticateUser(UserInterface $user)
{
// TODO: Implement authenticateUser() method.
}
}
class ThirdPartyUserAuthentication implements LoginInterface
{
public function authenticateUser(UserInterface $user)
{
// TODO: Implement authenticateUser() method.
}
}
class LoginService
{
public function login(LoginInterface $loginService, UserInterface $user)
{
$loginService->authenticateUser($user);
}
}
As you can see, the LoginService
class is agnostic of which authentication method (via web, via google or twitter, etc).
Payments API implemented with a switch - OCP Violation 👎
A very common case is when we have a switch()
, where each case performs a different action and there is the possibility that in the future we will continue adding more cases to the switch. Let's look at the following example.
Here we have a controller with a pay()
method, which is responsible for receiving the type of payment through the request and, depending on that type, the payment will be processed through one or another method found in the Payment class.
public function pay(Request $request)
{
$payment = new Payment();
switch ($request->type) {
case 'credit':
$payment->payWithCreditCard();
break;
case 'paypal':
$payment->payWithPaypal();
break;
default:
// Exception
break;
}
}
class PaymentRequest
{
public function payWithCreditCard()
{
// Logic to pay with a credit card...
}
public function payWithPaypal()
{
// Logic to pay with paypal...
}
}
This code has 2 big problems:
- We should add one more case for each new payment that we accept or delete a case in the event that we do not accept more payments through PayPal.
- All the methods that process the different types of payments are found in a single class, the Payment class. Therefore, when we add a new payment type or remove one, we should edit the Payment class, and as the
Open / Closed principle
says, this is not ideal. Like it is also violating the principle ofSingle Responsibility
.
This violation it's also related to a code smell call Switch Statements Smell if you want to know more about refactors or examples go into refactoring guru
Payments API implemented with a switch - Following OCP 👍
The first thing we could do to try to comply with the OCP is to create an interface with the pay()
method.
interface PayableInterface
{
public function pay();
}
Now we will proceed to create the classes that should implement these interfaces.
class CreditCardPayment implements PayableInterface
{
public function pay()
{
// Logic to pay with a credit card...
}
}
class PaypalPayment implements PayableInterface
{
public function pay()
{
// Logic to pay with paypal...
}
}
The next step would be to refactor our pay()
method.
public function pay(Request $request)
{
$paymentFactory = new PaymentFactory();
$payment = $paymentFactory->initialize($request->type);
return $payment->pay();
}
👁 As you can see, we have replaced the switch with a factory.
class PaymentFactory
{
public function initialize(string $type): PayableInterface
{
switch ($type) {
case 'credit':
return new CreditCardPayment();
case 'paypal':
return new PayPalPayment();
default:
throw new \Exception("Payment method not supported");
break;
}
}
}
Benefits of the Open / Closed Principle
- Extend the functionalities of the system, without touching the core of the system.
- We prevent breaking parts of the system by adding new functionalities.
- Ease of testing.
- Separation of the different logics.
🎨 Design patterns that we can find useful for OCP
L - Liskov Substitution Principle(LSP)
This principle was introduced by the guru and the first woman in America to earn a Ph.D. in Computer Science, Barbara Liskov. And it is a very interesting principle.
According to Wikipedia. Liskov's Substitution Principle says that each class that inherits from another can be used as its parent without having to know the differences between them.
- Concepts:
- If S is a subtype of T, instances of T should be substitutable for instances of S without altering the program properties, That is, by having a hierarchy it means that we are establishing a contract in the father, so ensuring that this contract is maintained in the child will allow us to replace the father and the application will continue to work perfectly.
- How to accomplish:
- The behavior of the sub-classes must respect the contract established in the super-class.
- Maintain functional correctness to be able to apply OCP.
There are 3 important points that we have to keep in mind in order not to violate the Liskov principle:
- Not to strengthen the pre-conditions and not to weaken the post-conditions of the parent class (defensive programming).
- The invariants set in the base class must be kept in the subclasses.
- Cannot be a method in the subclass that goes against a behavior of the base class. This is called Historical Constraint.
Example
Shipping calculation
Let's say we have a Shipping class that is going to calculate the shipping cost of a product given its weight and destination.
class Shipping
{
public function calculateShippingCost($weightOfPackageKg, $destiny)
{
// Pre-condition:
if ($weightOfPackageKg <= 0) {
throw new \Exception('Package weight cannot be less than or equal to zero');
}
// We calculate the shipping cost by
$shippingCost = rand(5, 15);
// Post-condition
if ($shippingCost <= 0) {
throw new \Exception('Shipping price cannot be less than or equal to zero');
}
return $shippingCost;
}
}
Shipping calculation - LSP violation due to behavior change in daughter class 👎
class WorldWideShipping extends Shipping
{
public function calculateShippingCost($weightOfPackageKg, $destiny)
{
// Pre-condition
if ($weightOfPackageKg <= 0) {
throw new \Exception('Package weight cannot be less than or equal to zero');
}
// We strengthen the pre-conditions
if (empty($destiny)) {
throw new \Exception('Destiny cannot be empty');
}
// We calculate the shipping cost by
$shippingCost = rand(5, 15);
// By changing the post-conditions we allow there to be cases
// in which the shipping is 0
if ('Spain' === $destiny) {
$shippingCost = 0;
}
return $shippingCost;
}
}
The problem is that we generate with a class like the previous one is that we are exposing a similar API for programmers, but that has a different implementation.
This class will be the parent class of our example where its method to calculate the shipping price has a pre and a post condition (this way of programming using the pre and post conditions is called Defensive Programming).
For example, a programmer on our team is sure that the calculateShippingCost()
method, of the Shipping
class, allows null
destination and shipping costs greater than zero, so by wanting to use the WorldWideShipping
class, it could cause the system to burst, for example, if you want to use the result of calculateShippingCost()
in a slice or by giving it a null
destiny.
Therefore, the WorldWideShipping class is violating the Liskov substitution principle.
Shipping calculation - LSP violation due to change of invariants from child class 👎
The invariants are values of the parent class that cannot be modified by the child classes.
Let's say we want to modify the Shipping
class that we had before and we want to make the limit of the weight per kilos be 0 but that it is in a variable.
class Shipping
{
protected $weightGreaterThan = 0;
public function calculateShippingCost($weightOfPackageKg, $destiny)
{
// Pre-condition:
if ($weightOfPackageKg <= $this->weightGreaterThan) {
throw new \Exception("Package weight cannot be less than or equal to {$this->weightGreaterThan}");
}
// We calculate the shipping cost by
$shippingCost = rand(5, 15);
// Post-condition
if ($shippingCost <= 0) {
throw new \Exception('Shipping price cannot be less than or equal to zero');
}
return $shippingCost;
}
}
class WorldWideShipping extends Shipping
{
public function calculateShippingCost($weightOfPackageKg, $destiny)
{
// We modify the value of the parent class
$this->weightGreaterThan = 10;
// Pre-condition
if ($weightOfPackageKg <= $this->weightGreaterThan) {
throw new \Exception("Package weight cannot be less than or equal to {$this->weightGreaterThan}");
}
// Previous code...
}
}
Shipping calculation - Following LSP by change of invariants from child class 👍
The easiest way to avoid this would be to simply create the variable $weightOfPackageKg
as a private constant if our version of PHP (7.1.0) allows it but by creating that private variable.
Historical Restrictions
Historical restrictions say that there cannot be a method in the child class that goes against a behavior of its parent class.
That is, if in the parent class there is the FixedTax()
method, then the ModifyTax()
method cannot exist in the child class. Or didn't they teach you not to disobey your parents? 😆.
For a method of the subclass to modify the value of a property of the base class is a violation of the Liskov principle because classes must be able to change the value of their properties only (Encapsulation).
The easiest way not to break the LSP
The best way not to break LSP is by using Interfaces. Instead of extending our child classes from a parent class.
interface CalculabeShippingCost
{
public function calculateShippingCost($weightOfPackageKg, $destiny);
}
class WorldWideShipping implements CalculabeShippingCost
{
public function calculateShippingCost($weightOfPackageKg, $destiny)
{
// Implementation of logic
}
}
By using interfaces you can implement methods that various classes have in common, but each method will have its own implementation, its own pre and post conditions, its own invariants, etc. We are not tied to a parent class.
⚠️ This does not mean that we start using interfaces everywhere, although they are very good. But sometimes it is better to use base classes and other times interfaces. It all depends on the situation.
Interfaces 🆚 Abstract Class
- Interface benefits
- Does not modify the hierarchy tree
- Allows to implement N Interfaces
- Benefits of Abstract Class
- It allows to develop the
Template Method
¹ pattern by pushing the logic to the model. Problem: Difficulty tracing who the actors are and when capturing errors - Private getters (Tell-Don't-Ask principle)
- It allows to develop the
¹. Design pattern Template Method: It states that in the abstract class we would define a method body that defines what operation we are going to perform, but we would be calling some methods defined as abstract (delegating the implementation to the children). But beware! 👀 this implies a loss of traceability of our code.
Conclusion of Interfaces 🆚 Abstract Class
When do we use Interfaces?: When we are going to decouple between layers.
When do we use Abstract?: In certain cases for Domain Models (Domain models not ORM models, to avoid anemic domain models)
🎨 Design patterns that can be useful to us in the LSP
I - Interface Segregation Principle (ISP)
💡 A client should only know the methods they are going to use and not those that they are not going to use.
Basically, what this principle refers to is that we should not create classes with thousands of methods where it ends up being a huge file. Since we are generating a monster class, where most of the time we will only use some of its methods each time. And for that it refers to the need for interfaces, it is also important to understand that this helps a lot at the Single Responsibility Principle(SRP).
- How to accomplish
- Define interface contracts based on the clients that use them and not on the implementations that we could have (The interfaces belong to the clients).
- Avoid Header Interfaces by promoting Role Interfaces
- Purpose or gain:
- High cohesion and low structural coupling
Header Interfaces
Martin fowler in the article HeaderInterface sustain.
💬 A header interface is an explicit interface that mimics the implicit public interface of a class. Essentially you take all the public methods of a class and declare them in an interface. You can then supply an alternative implementation for the class. This is the opposite of a RoleInterface - I discuss more details and the pros and cons there.
Role Interfaces
Martin fowler in the article RoleInterface sustain.
💬 A role interface is defined by looking at a specific interaction between suppliers and consumers. A supplier component will usually implement several role interfaces, one for each of these patterns of interaction. This contrasts to a HeaderInterface, where the supplier will only have a single interface.
Examples
Simple Example
We want to be able to send notifications via email, Slack, or txt file. What signature will the interface have? 📨
- a) $notifier($content) ✔️
- b) $notifier($slackChannel, $messageTitle, $messageContent, $messageStatus) ❌
- c) $notifier($recieverEmail, $emailSubject, $emailContent) ❌
- d) $notifier($destination, $subject, $content) ❌
- e) $notifier($filename, $tag, $description) ❌
We can rule out that options B, C and E, since Header Interface would be based on the implementation (for Slack, email and file respectively).
In the case of option D, we could consider it invalid given that the type $destination
It does not offer us any specificity (we do not know if it is an email, a channel ...).
Finally, in option A, we would only be sending the content, so the particularities of each of the types of notification would have to be given in the constructor (depending on the use case you could not always).
👁 The interfaces belong to the clients and not to those who implement them.
Example Developer | QA | PM - ISP violation due to excess responsibilities and poor abstraction 👎
A simple example would be the following situation. Let's imagine that we have developers, a QA team and a project manager who have to determine whether to program.
Let's say the programmer can program and test, while the QA can only test.
interface Workable
{
public function canCode();
public function code();
public function test();
}
class Developer implements Workable
{
public function canCode()
{
return true;
}
public function code()
{
return 'coding';
}
public function test()
{
return 'testing in localhost';
}
}
class Tester implements Workable
{
public function canCode()
{
return false;
}
public function code()
{
// El QA no puede programar
throw new Exception('Opps! I can not code');
}
public function test()
{
return 'testing in test server';
}
}
class ProjectManagement
{
public function processCode(Workable $member)
{
if ($member->canCode()) {
$member->code();
}
}
}
If we pay attention we will see that the Tester
class has a method that does not correspond to it since it is not called and if it is called it would give us an Exception
.
So we should make a small refactor to be able to comply with the principle of segregation of interfaces.
Example Developer | QA | PM - Following ISP 👍
The first thing is to identify what actions we have to perform, design the interfaces and assign these interfaces to the corresponding actors depending on the use case.
interface Codeable
{
public function code();
}
interface Testable
{
public function test();
}
class Programmer implements Codeable, Testable
{
public function code()
{
return 'coding';
}
public function test()
{
return 'testing in localhost';
}
}
class Tester implements Testable
{
public function test()
{
return 'testing in test server';
}
}
class ProjectManagement
{
public function processCode(Codeable $member)
{
$member->code();
}
}
This code does comply with the principle of segregation of interfaces. As with the previous principles.
🎨 Design patterns that can be useful to us in the ISP
D - Dependency Inversion Principle (DIP)
I must first make it clear that Dependency Injection is NOT the same as Dependency Inversion. Dependency inversion is a principle, while dependency injection is a design pattern.
💡 High-level modules should not depend on low-level ones. Both should depend on abstractions
- How to accomplish
- Inject dependencies (parameters received in constructor).
- Rely on the interfaces (contracts) of these dependencies and not on specific implementations.
- LSP as a premise.
- Purpose or gain:
- Facilitate the modification and replacement of implementations.
- Better class testability
The principle of dependency injection tries to maintain a low coupling.
Laravel controller example
Let's say we have a UserController
. What in its index
method what it does is return a JSON
list of users with the users created the previous day.
Laravel controller - DIP Violation 👎
public function index()
{
$users = new User();
$users = $users->where('created_at', Carbon::yesterday())->get();
return response()->json(['users' => $users]);
}
}
This code wouldn't be bad, because it would clearly work. But at the same time it would generate the following problems:
- We cannot reuse the code as we are tied to Eloquent.
- It is difficult to test the methods that instantiate one or several objects (high coupling), since it is difficult to verify that it is failing.
- It breaks the principle of single responsibility, because, in addition to the method doing its job, it also has to create the objects in order to do its job.
Laravel controller - Following the DIP 👍
interface UserRepositoryInterface
{
// 👁 I am returning an array
// but it should return Domain Models
public function getUserFromYesterday(DateInterface $date): array;
}
class UserEloquentRepository implements UserRepositoryInterface
{
public function getUserFromYesterday(DateInterface $date): array
{
return User::where('created_at', '>', $date)
->get()
->toArray();
}
}
class UserSqlRepository implements UserRepositoryInterface
{
public function getUserFromYesterday(DateInterface $date): array
{
return \DB::table('users')
->where('created_at', '>', $date)
->get()
->toArray();
}
}
class UserCsvRepository implements UserRepositoryInterface
{
public function getUserFromYesterday(DateInterface $date): array
{
// 👁 I am accessing the infrastructure
// from the same method maybe not the best
$fileName = "users_created_{$date}.csv";
$fileHandle = fopen($fileName, 'r');
while (($users[] = fgetscsv($fileHandle, 0, ",")) !== false) {
}
fclose($fileHandle);
return $users;
}
}
As we can see, all classes implement the UserRespositoryInterface
interface. And this gives us the freedom to get the users either from Eloquent
, fromSQL
or from a CSV
👏😲 file.
This is fine and would work in a normal application, but how do we make the Laravel controller receive that repository in its index method?
The answer is registering the interface with which class it has by default.
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(
'App\Repositories\Contracts\UserRepositoryInterface',
'App\Repositories\UserCsvRepository'
);
}
}
In other frameworks like Symfony it can be done using PHP DI.
Conclusions
In order to make a more maintainable, reusable and testable code we should try to implement these principles. Like we should know the design patterns that could be useful when we use these principles.
Webgraphy
🇪🇸
- Francisco - Twitter
- Por qué NO usar getters y setters | Tell don't ask - Twitter - Youtube - Courses
- Errores comunes al diseñar Interfaces - #SOLID - ISP - Twitter - Youtube - Courses
- Principio de Segregación de Interfaces - SOLID - Twitter - Youtube - Courses
- Herminio Heredia Santos - domina la inyeccion de dependencias de laravel - Twitter - LinkedLn
- Laravel tip - Desacoplando Laravel de tu aplicación
(🇬🇧/🇺🇸)
Top comments (36)
Hello Emmanuel, great article, really. I'm looking for a complete guide and with this I really find it and help me to learning these principles and how to implement on my work. I have a question, on LCP principle, and example two (invariants), do you explain: "The easiest way to avoid this would be to simply create the variable $weightOfPackageKg as a private constant" -> do you mean really to this var, or the class Shipping propiety $weightGreaterThan ?
Great explanation! The code samples (correct vs. incorrect) are great - they really helped illustrate the principles well. I am definitely bookmarking this. Thanks for putting so much work into this and sharing.
First of all thank you for the comment.
And on the other hand to tell you that thanks to comments like this I have the intention of continuing to post like this
Wow, this is really great.
This is the best explanation of SOLID Principles I found on the internet, I really like the drawing illustration as it explain well.
Very good explanation of the SOLID principles. Thank you!
In the middle of 2021-pandemic-chaos, this post came to refresh and enhance some pieces of my daily coding. I really appreciated the effort and time invested on this keystone summary as it's SOLID for coding in a good way.
I'm really happy that the post helped you
I find this article by mistake, but it's awesome explanation of SOLID principles. By myself I understand only first two. The other three are nicely explained.
First of all, thank you for the feedback, I would like the post to have more visibility but you can see the algorithm of dev.to it's not helping
Your article showed up in "Top 7 Posts From Last Week" (which is how I found it), so the algorithm doesn't seem to be hurting you. 🙂
Hahahah, Yes so it seems, but I have a small reader in which I read the dev.to via the API, and I haven't see my post on the api :D
Pretty good practical explanation! Here I go into philosophical detail about why those principles are that way, how they relate to Separation of Concerns, and how they are coherent with the natural way things are.
dev.to/xedinunknown/separation-of-...
😍 Love it
Cool article Emmanuel, thank you.
PS:
PaymentFactory.initialize()
can havePayableInterface
return type, by the way.Updated the signature of the method to
public function initialize(string $type): PayableInterface
Thank you for the feedbackLearning by making mistakes is my mojo, the way you explained SOLID is the best to teach me how it's really working. Thank you
🙏 thanks really for saying that, I'm glad my article can help you