SOLID: Difference between revisions
imported>Unknown user No edit summary |
No edit summary |
||
| (4 intermediate revisions by 3 users not shown) | |||
| Line 1: | Line 1: | ||
SOLID is an acronym that represents a set of five design principles aimed at making software designs more understandable, flexible, and maintainable. These principles are particularly useful in object-oriented design and programming. When followed properly, they can help in reducing tight coupling, making the system more modular, and easing the refactoring process. The SOLID principles are Single Responsibility, Open/closed, Liskov substitution, Interface segregation and Dependency inversion: | |||
[[File:UserDocumentation_Knowledge_Central_KC_IT_IT_backend_team_IT_backend_policies_IT_backend_architecture_SOLIDimage-20250419-100031.png|400px|center]] | |||
<span id="single-responsibility-principle-srp"></span> | |||
= Single Responsibility Principle (SRP) = | |||
A class should have only one reason to change. This implies that a class should only have one responsibility or one job. By adhering to this principle, you minimize the chance that changes in one part of the software will affect unrelated parts. This in turn results in a lower risk of introducing bugs and makes the system more maintainable. | |||
When classes (or even functions and modules) take on multiple responsibilities, it often becomes harder to modify, understand, and maintain them. These multifunctional classes often lead to what is known as "code smells" – indicators that the code might need refactoring. One common code smell is a class that's grown too large because it's trying to do too much. | |||
By focusing on a single responsibility, you can ensure that: | |||
* The class has a clear, well-defined role. | |||
* Changes to the class will only be required for one reason, reducing the potential impact of changes. | |||
* The class can be easily understood, tested, and maintained. | |||
<span id="php-example"></span> | |||
=== PHP Example === | |||
Let's consider a simple user management system: | |||
'''Not following SRP''': | |||
<syntaxhighlight lang="php">class User { | |||
private $email; | |||
private $password; | |||
public function __construct($email, $password) { | |||
$this->email = $email; | |||
$this->password = $password; | |||
} | |||
public function register() { | |||
// Database registration logic here | |||
} | |||
public function sendConfirmationEmail() { | |||
// Email sending logic here | |||
} | |||
}</syntaxhighlight> | |||
In the example above, the User class has responsibilities for both database interaction (registration) and email handling. This violates the SRP. | |||
'''Following SRP''': | |||
<syntaxhighlight lang="php">class User { | |||
private $email; | |||
private $password; | |||
public function __construct($email, $password) { | |||
$this->email = $email; | |||
$this->password = $password; | |||
} | |||
public function register(UserDatabase $db) { | |||
$db->addUser($this); | |||
} | |||
} | |||
class UserDatabase { | |||
public function addUser(User $user) { | |||
// Database registration logic here | |||
} | |||
} | |||
class UserEmailer { | |||
public function sendConfirmationEmail(User $user) { | |||
// Email sending logic here | |||
} | |||
}</syntaxhighlight> | |||
In the refactored example: | |||
* The User class focuses on representing a user and interacting with the database. | |||
* The UserDatabase class is responsible for all database-related operations concerning users. | |||
* The UserEmailer class takes care of sending emails related to users. | |||
* Each class now has a single, clear responsibility, adhering to the SRP. This makes it easier to update or modify functionalities related to database operations or email sending without touching the core User representation. | |||
<span id="openclosed-principle-ocp"></span> | |||
= Open/Closed Principle (OCP) = | |||
The Open/Closed Principle asserts that software entities (like classes, modules, and functions) should be open for extension but closed for modification. This means that the behavior of a module can be extended without modifying its source code. | |||
When we need to make changes in existing code every time a new feature or change request comes in, it increases the risk of introducing bugs into the existing functionality. Following the OCP helps in promoting decoupling, which in turn reduces such risks and enhances reusability. | |||
<span id="php-example-1"></span> | |||
=== PHP Example === | |||
Imagine you have a class that calculates the area of several shapes: | |||
'''Without OCP''': | |||
<syntaxhighlight lang="php">class AreaCalculator { | |||
public function calculate($shapes) { | |||
$area = 0; | |||
foreach ($shapes as $shape) { | |||
if ($shape instanceof Rectangle) { | |||
$area += $shape->width * $shape->height; | |||
} elseif ($shape instanceof Circle) { | |||
$area += $shape->radius * $shape->radius * pi(); | |||
} | |||
// What if we want to add Triangle or other shapes? We'd have to modify this class. | |||
} | |||
return $area; | |||
} | |||
} | |||
class Rectangle { | |||
public $width; | |||
public $height; | |||
} | |||
class Circle { | |||
public $radius; | |||
}</syntaxhighlight> | |||
In the above example, every time a new shape is introduced, we have to modify the AreaCalculator class, violating the OCP. | |||
'''With OCP''' we can use polymorphism and interfaces: | |||
<syntaxhighlight lang="php">interface Shape { | |||
public function area(); | |||
} | |||
class Rectangle implements Shape { | |||
public $width; | |||
public $height; | |||
public function area() { | |||
return $this->width * $this->height; | |||
} | |||
} | |||
class Circle implements Shape { | |||
public $radius; | |||
public function area() { | |||
return $this->radius * $this->radius * pi(); | |||
} | |||
} | |||
class AreaCalculator { | |||
public function calculate($shapes) { | |||
$area = 0; | |||
foreach ($shapes as $shape) { | |||
$area += $shape->area(); | |||
} | |||
return $area; | |||
} | |||
}</syntaxhighlight> | |||
Now, if you want to introduce a new shape, you don't need to modify the AreaCalculator class. Instead, you'll just create a new shape class implementing the Shape interface. This way, the AreaCalculator remains closed for modification but open for extension. | |||
For instance, to introduce a triangle: | |||
<syntaxhighlight lang="php">class Triangle implements Shape { | |||
public $base; | |||
public $height; | |||
public function area() { | |||
return 0.5 * $this->base * $this->height; | |||
} | |||
}</syntaxhighlight> | |||
Following the OCP like this makes the system more robust against changes and promotes a more modular and scalable design. | |||
<span id="liskov-substitution-principle-lsp"></span> | |||
== Liskov Substitution Principle (LSP) == | |||
If a program is using a base class, it should be able to use any of its derived or child classes without knowing it and without the program's correctness being affected. | |||
Named after computer scientist Barbara Liskov, the principle primarily implies that objects of a superclass should be replaceable by objects of a subclass without affecting the program's correctness. | |||
Implications of LSP: | |||
* Subtypes must maintain the invariants of the supertype. | |||
* Derived classes should never override and provide a lesser implementation, especially in the context of polymorphism. | |||
* Derived classes should enhance or refine the behavior, not restrict or alter it. | |||
<span id="php-examples"></span> | |||
=== PHP Examples === | |||
'''Violation of LSP''': Suppose you have a Bird class and a derived class Penguin. Penguins can't fly, so overriding the fly method in the Penguin class could violate the LSP if the base class assumes all birds can fly. | |||
<syntaxhighlight lang="php">class Bird { | |||
public function fly() { | |||
// logic to let the bird fly | |||
} | |||
} | |||
class Penguin extends Bird { | |||
public function fly() { | |||
// Penguins can't fly! | |||
throw new Exception("Can't fly"); | |||
} | |||
} | |||
function makeBirdFly(Bird $bird) { | |||
$bird->fly(); // This will break for Penguin objects | |||
}</syntaxhighlight> | |||
If you pass an instance of Penguin to makeBirdFly, it'll throw an exception, which violates the Liskov Substitution Principle. | |||
'''Adhering to LSP''': A better approach would be to restructure the classes and perhaps use interfaces to clearly define the abilities. | |||
<syntaxhighlight lang="php">interface FlyingBird { | |||
public function fly(); | |||
} | |||
interface WalkingBird { | |||
public function walk(); | |||
} | |||
class Sparrow implements FlyingBird, WalkingBird { | |||
public function fly() { | |||
// Sparrow can fly | |||
} | |||
public function walk() { | |||
// Sparrow can walk | |||
} | |||
} | |||
class Penguin implements WalkingBird { | |||
public function walk() { | |||
// Penguin can walk | |||
} | |||
} | |||
function makeBirdFly(FlyingBird $bird) { | |||
$bird->fly(); // This is now safe and adheres to LSP | |||
}</syntaxhighlight> | |||
By restructuring this way, you avoid placing flying behavior on birds that can't fly. The makeBirdFly function is also more robust because it expects an instance of FlyingBird, ensuring that the bird passed can indeed fly. | |||
In summary, the Liskov Substitution Principle encourages a robust hierarchy where derived classes genuinely extend the behavior of their base classes without introducing unexpected and erroneous behaviors. | |||
<span id="interface-segregation-principle-isp"></span> | |||
== Interface Segregation Principle (ISP) == | |||
Clients should not be forced to implement interfaces they do not use. This principle suggests that it is better to have several specific interfaces rather than one general-purpose, all-encompassing interface. | |||
When an interface becomes too large and requires implementing classes to provide methods that they don't use, it can lead to a cumbersome design. A class having unused methods due to its adherence to a bulky interface can confuse developers, leading to potential misuses and increasing the chance of defects. | |||
<span id="php-examples-1"></span> | |||
=== PHP Examples === | |||
'''Violation of ISP''' Suppose we have an interface representing operations for a basic printer: | |||
<syntaxhighlight lang="php">interface Printer { | |||
public function printDocument(); | |||
public function scanDocument(); | |||
public function faxDocument(); | |||
} | |||
class InkjetPrinter implements Printer { | |||
public function printDocument() { | |||
// print logic | |||
} | |||
public function scanDocument() { | |||
// scan logic | |||
} | |||
public function faxDocument() { | |||
// fax logic | |||
} | |||
} | |||
class SimplePrinter implements Printer { | |||
public function printDocument() { | |||
// print logic | |||
} | |||
public function scanDocument() { | |||
// This printer doesn't support scanning! | |||
} | |||
public function faxDocument() { | |||
// This printer doesn't support faxing! | |||
} | |||
}</syntaxhighlight> | |||
In the above example, SimplePrinter is forced to provide implementations for scanDocument and faxDocument, even though it doesn't support these functionalities. This violates ISP. | |||
'''Adherence to ISP''' To follow ISP, we would break the Printer interface into smaller, more focused interfaces: | |||
<syntaxhighlight lang="php">interface Printer { | |||
public function printDocument(); | |||
} | |||
interface Scanner { | |||
public function scanDocument(); | |||
} | |||
interface Fax { | |||
public function faxDocument(); | |||
} | |||
class InkjetPrinter implements Printer, Scanner, Fax { | |||
public function printDocument() { | |||
// print logic | |||
} | |||
public function scanDocument() { | |||
// scan logic | |||
} | |||
public function faxDocument() { | |||
// fax logic | |||
} | |||
} | |||
class SimplePrinter implements Printer { | |||
public function printDocument() { | |||
// print logic | |||
} | |||
}</syntaxhighlight> | |||
Here, SimplePrinter only implements the Printer interface and is not burdened by methods it doesn't need or use. The design becomes clearer and more modular, with each interface focused on a single responsibility. | |||
In summary, the Interface Segregation Principle encourages the design of narrow, purpose-focused interfaces, ensuring that implementing classes are not saddled with irrelevant methods. This principle enhances code clarity and modularity and reduces the potential for errors. | |||
<span id="dependency-inversion-principle-dip"></span> | |||
== Dependency Inversion Principle (DIP) == | |||
The Dependency Inversion Principle consists of two main guidelines: | |||
* High-level modules (which typically implement core business rules or logic) should not depend on low-level modules (which usually handle data access, external services, or detailed system operations). Both should depend on abstractions (like interfaces or abstract classes). | |||
* Abstractions should not depend on details. The details (concrete implementations) should depend on abstractions. | |||
The main idea behind DIP is to reduce the direct dependencies between high-level modules and low-level modules, making the system more modular, easier to refactor, and more adaptable to changes. By following this principle, we aim to achieve a more flexible and decoupled architecture. | |||
<span id="php-example-2"></span> | |||
=== PHP Example === | |||
Imagine you're building an application that sends notifications to users. A naive approach might tie your high-level notification system directly to a concrete method of delivery, say, an email. | |||
'''Without Dependency Inversion''' | |||
<syntaxhighlight lang="php">class EmailService { | |||
public function sendEmail($message, $user) { | |||
// Sends an email | |||
} | |||
} | |||
class Notification { | |||
private $service; | |||
public function __construct() { | |||
$this->service = new EmailService(); | |||
} | |||
public function notify($message, $user) { | |||
$this->service->sendEmail($message, $user); | |||
} | |||
}</syntaxhighlight> | |||
In the example above, the high-level module Notification directly depends on the low-level module EmailService. If you want to change the notification method (e.g., SMS or push notifications), the Notification class will need to be modified, violating the Open/Closed Principle. | |||
'''With Dependency Inversion''' | |||
<syntaxhighlight lang="php">interface NotificationService { | |||
public function send($message, $user); | |||
} | |||
class EmailService implements NotificationService { | |||
public function send($message, $user) { | |||
// Sends an email | |||
} | |||
} | |||
class SMSService implements NotificationService { | |||
public function send($message, $user) { | |||
// Sends an SMS | |||
} | |||
} | |||
class Notification { | |||
private $service; | |||
public function __construct(NotificationService $service) { | |||
$this->service = $service; | |||
} | |||
public function notify($message, $user) { | |||
$this->service->send($message, $user); | |||
} | |||
}</syntaxhighlight> | |||
In this refactored example: | |||
* We defined an interface NotificationService which represents the abstraction. | |||
* Both EmailService and SMSService implement this interface. | |||
* The Notification class now depends on the NotificationService interface (an abstraction) rather than any specific implementation. This allows you to easily swap out notification methods without changing the Notification class. | |||
With this structure, adding a new notification method (e.g., push notifications) would only involve creating a new class that implements NotificationService, making the system much more extensible and in line with the Dependency Inversion Principle. | |||
[[Category:IT backend architecture]] | |||
Latest revision as of 17:14, 22 October 2025
SOLID is an acronym that represents a set of five design principles aimed at making software designs more understandable, flexible, and maintainable. These principles are particularly useful in object-oriented design and programming. When followed properly, they can help in reducing tight coupling, making the system more modular, and easing the refactoring process. The SOLID principles are Single Responsibility, Open/closed, Liskov substitution, Interface segregation and Dependency inversion:

Single Responsibility Principle (SRP)
A class should have only one reason to change. This implies that a class should only have one responsibility or one job. By adhering to this principle, you minimize the chance that changes in one part of the software will affect unrelated parts. This in turn results in a lower risk of introducing bugs and makes the system more maintainable.
When classes (or even functions and modules) take on multiple responsibilities, it often becomes harder to modify, understand, and maintain them. These multifunctional classes often lead to what is known as "code smells" – indicators that the code might need refactoring. One common code smell is a class that's grown too large because it's trying to do too much.
By focusing on a single responsibility, you can ensure that:
- The class has a clear, well-defined role.
- Changes to the class will only be required for one reason, reducing the potential impact of changes.
- The class can be easily understood, tested, and maintained.
PHP Example
Let's consider a simple user management system:
Not following SRP:
class User {
private $email;
private $password;
public function __construct($email, $password) {
$this->email = $email;
$this->password = $password;
}
public function register() {
// Database registration logic here
}
public function sendConfirmationEmail() {
// Email sending logic here
}
}In the example above, the User class has responsibilities for both database interaction (registration) and email handling. This violates the SRP.
Following SRP:
class User {
private $email;
private $password;
public function __construct($email, $password) {
$this->email = $email;
$this->password = $password;
}
public function register(UserDatabase $db) {
$db->addUser($this);
}
}
class UserDatabase {
public function addUser(User $user) {
// Database registration logic here
}
}
class UserEmailer {
public function sendConfirmationEmail(User $user) {
// Email sending logic here
}
}In the refactored example:
- The User class focuses on representing a user and interacting with the database.
- The UserDatabase class is responsible for all database-related operations concerning users.
- The UserEmailer class takes care of sending emails related to users.
- Each class now has a single, clear responsibility, adhering to the SRP. This makes it easier to update or modify functionalities related to database operations or email sending without touching the core User representation.
Open/Closed Principle (OCP)
The Open/Closed Principle asserts that software entities (like classes, modules, and functions) should be open for extension but closed for modification. This means that the behavior of a module can be extended without modifying its source code.
When we need to make changes in existing code every time a new feature or change request comes in, it increases the risk of introducing bugs into the existing functionality. Following the OCP helps in promoting decoupling, which in turn reduces such risks and enhances reusability.
PHP Example
Imagine you have a class that calculates the area of several shapes:
Without OCP:
class AreaCalculator {
public function calculate($shapes) {
$area = 0;
foreach ($shapes as $shape) {
if ($shape instanceof Rectangle) {
$area += $shape->width * $shape->height;
} elseif ($shape instanceof Circle) {
$area += $shape->radius * $shape->radius * pi();
}
// What if we want to add Triangle or other shapes? We'd have to modify this class.
}
return $area;
}
}
class Rectangle {
public $width;
public $height;
}
class Circle {
public $radius;
}In the above example, every time a new shape is introduced, we have to modify the AreaCalculator class, violating the OCP.
With OCP we can use polymorphism and interfaces:
interface Shape {
public function area();
}
class Rectangle implements Shape {
public $width;
public $height;
public function area() {
return $this->width * $this->height;
}
}
class Circle implements Shape {
public $radius;
public function area() {
return $this->radius * $this->radius * pi();
}
}
class AreaCalculator {
public function calculate($shapes) {
$area = 0;
foreach ($shapes as $shape) {
$area += $shape->area();
}
return $area;
}
}Now, if you want to introduce a new shape, you don't need to modify the AreaCalculator class. Instead, you'll just create a new shape class implementing the Shape interface. This way, the AreaCalculator remains closed for modification but open for extension.
For instance, to introduce a triangle:
class Triangle implements Shape {
public $base;
public $height;
public function area() {
return 0.5 * $this->base * $this->height;
}
}Following the OCP like this makes the system more robust against changes and promotes a more modular and scalable design.
Liskov Substitution Principle (LSP)
If a program is using a base class, it should be able to use any of its derived or child classes without knowing it and without the program's correctness being affected.
Named after computer scientist Barbara Liskov, the principle primarily implies that objects of a superclass should be replaceable by objects of a subclass without affecting the program's correctness.
Implications of LSP:
- Subtypes must maintain the invariants of the supertype.
- Derived classes should never override and provide a lesser implementation, especially in the context of polymorphism.
- Derived classes should enhance or refine the behavior, not restrict or alter it.
PHP Examples
Violation of LSP: Suppose you have a Bird class and a derived class Penguin. Penguins can't fly, so overriding the fly method in the Penguin class could violate the LSP if the base class assumes all birds can fly.
class Bird {
public function fly() {
// logic to let the bird fly
}
}
class Penguin extends Bird {
public function fly() {
// Penguins can't fly!
throw new Exception("Can't fly");
}
}
function makeBirdFly(Bird $bird) {
$bird->fly(); // This will break for Penguin objects
}If you pass an instance of Penguin to makeBirdFly, it'll throw an exception, which violates the Liskov Substitution Principle.
Adhering to LSP: A better approach would be to restructure the classes and perhaps use interfaces to clearly define the abilities.
interface FlyingBird {
public function fly();
}
interface WalkingBird {
public function walk();
}
class Sparrow implements FlyingBird, WalkingBird {
public function fly() {
// Sparrow can fly
}
public function walk() {
// Sparrow can walk
}
}
class Penguin implements WalkingBird {
public function walk() {
// Penguin can walk
}
}
function makeBirdFly(FlyingBird $bird) {
$bird->fly(); // This is now safe and adheres to LSP
}By restructuring this way, you avoid placing flying behavior on birds that can't fly. The makeBirdFly function is also more robust because it expects an instance of FlyingBird, ensuring that the bird passed can indeed fly.
In summary, the Liskov Substitution Principle encourages a robust hierarchy where derived classes genuinely extend the behavior of their base classes without introducing unexpected and erroneous behaviors.
Interface Segregation Principle (ISP)
Clients should not be forced to implement interfaces they do not use. This principle suggests that it is better to have several specific interfaces rather than one general-purpose, all-encompassing interface.
When an interface becomes too large and requires implementing classes to provide methods that they don't use, it can lead to a cumbersome design. A class having unused methods due to its adherence to a bulky interface can confuse developers, leading to potential misuses and increasing the chance of defects.
PHP Examples
Violation of ISP Suppose we have an interface representing operations for a basic printer:
interface Printer {
public function printDocument();
public function scanDocument();
public function faxDocument();
}
class InkjetPrinter implements Printer {
public function printDocument() {
// print logic
}
public function scanDocument() {
// scan logic
}
public function faxDocument() {
// fax logic
}
}
class SimplePrinter implements Printer {
public function printDocument() {
// print logic
}
public function scanDocument() {
// This printer doesn't support scanning!
}
public function faxDocument() {
// This printer doesn't support faxing!
}
}In the above example, SimplePrinter is forced to provide implementations for scanDocument and faxDocument, even though it doesn't support these functionalities. This violates ISP.
Adherence to ISP To follow ISP, we would break the Printer interface into smaller, more focused interfaces:
interface Printer {
public function printDocument();
}
interface Scanner {
public function scanDocument();
}
interface Fax {
public function faxDocument();
}
class InkjetPrinter implements Printer, Scanner, Fax {
public function printDocument() {
// print logic
}
public function scanDocument() {
// scan logic
}
public function faxDocument() {
// fax logic
}
}
class SimplePrinter implements Printer {
public function printDocument() {
// print logic
}
}Here, SimplePrinter only implements the Printer interface and is not burdened by methods it doesn't need or use. The design becomes clearer and more modular, with each interface focused on a single responsibility.
In summary, the Interface Segregation Principle encourages the design of narrow, purpose-focused interfaces, ensuring that implementing classes are not saddled with irrelevant methods. This principle enhances code clarity and modularity and reduces the potential for errors.
Dependency Inversion Principle (DIP)
The Dependency Inversion Principle consists of two main guidelines:
- High-level modules (which typically implement core business rules or logic) should not depend on low-level modules (which usually handle data access, external services, or detailed system operations). Both should depend on abstractions (like interfaces or abstract classes).
- Abstractions should not depend on details. The details (concrete implementations) should depend on abstractions.
The main idea behind DIP is to reduce the direct dependencies between high-level modules and low-level modules, making the system more modular, easier to refactor, and more adaptable to changes. By following this principle, we aim to achieve a more flexible and decoupled architecture.
PHP Example
Imagine you're building an application that sends notifications to users. A naive approach might tie your high-level notification system directly to a concrete method of delivery, say, an email.
Without Dependency Inversion
class EmailService {
public function sendEmail($message, $user) {
// Sends an email
}
}
class Notification {
private $service;
public function __construct() {
$this->service = new EmailService();
}
public function notify($message, $user) {
$this->service->sendEmail($message, $user);
}
}In the example above, the high-level module Notification directly depends on the low-level module EmailService. If you want to change the notification method (e.g., SMS or push notifications), the Notification class will need to be modified, violating the Open/Closed Principle.
With Dependency Inversion
interface NotificationService {
public function send($message, $user);
}
class EmailService implements NotificationService {
public function send($message, $user) {
// Sends an email
}
}
class SMSService implements NotificationService {
public function send($message, $user) {
// Sends an SMS
}
}
class Notification {
private $service;
public function __construct(NotificationService $service) {
$this->service = $service;
}
public function notify($message, $user) {
$this->service->send($message, $user);
}
}In this refactored example:
- We defined an interface NotificationService which represents the abstraction.
- Both EmailService and SMSService implement this interface.
- The Notification class now depends on the NotificationService interface (an abstraction) rather than any specific implementation. This allows you to easily swap out notification methods without changing the Notification class.
With this structure, adding a new notification method (e.g., push notifications) would only involve creating a new class that implements NotificationService, making the system much more extensible and in line with the Dependency Inversion Principle.