DEV Community

Cover image for Understanding SOLID principles
Md Abu Musa
Md Abu Musa

Posted on

Understanding SOLID principles

The SOLID principles are five key design principles that help developers build scalable, maintainable, and understandable code. Here’s an overview of each principle, followed by a PHP example.


1. Single Responsibility Principle (SRP)

  • Concept: A class should have only one reason to change, meaning it should handle a single responsibility or functionality. When a class has only one responsibility, it becomes easier to maintain and test, reducing the chances of unexpected bugs.

  • Why It Matters: By keeping each class focused on a single task, we can ensure that changes in one area of the program don’t have unintended side effects elsewhere. This separation also makes it easier to understand the code since each class has a specific purpose.

  • Example in PHP:
    Suppose we have an Invoice class that initially handles both invoice calculations and sending notifications:

     class Invoice {
         public function calculateTotal() {
             // Logic to calculate invoice total
         }
    
         public function sendEmailNotification() {
             // Logic to send email
         }
     }
    

    Here, the Invoice class has two responsibilities: calculating totals and sending notifications. Applying SRP, we would separate these concerns:

     class InvoiceCalculator {
         public function calculateTotal($items) {
             // Logic to calculate invoice total
         }
     }
    
     class InvoiceNotifier {
         public function sendEmailNotification($invoice) {
             // Logic to send email
         }
     }
    

    Now each class has a single responsibility: InvoiceCalculator handles calculations, and InvoiceNotifier handles notifications.


2. Open-Closed Principle (OCP)

  • Concept: Software entities (like classes and modules) should be open for extension but closed for modification. This means you can add new features by extending functionality rather than changing existing code.
  • Why It Matters: Following OCP reduces the risk of introducing bugs into existing functionality. It also allows for new features to be added in a flexible, modular way that doesn’t affect existing, stable code.

  • Example in PHP:
    Imagine a payment system that initially only supported PayPal payments:

     class PaymentProcessor {
         public function processPaypalPayment($amount) {
             // Process PayPal payment
         }
     }
    

    If we now want to add a new payment method (e.g., credit card), changing the PaymentProcessor class would violate OCP. Instead, we can create an interface and add new payment methods without modifying the existing code:

     interface PaymentProcessor {
         public function processPayment($amount);
     }
    
     class PayPalPayment implements PaymentProcessor {
         public function processPayment($amount) {
             // Process PayPal payment
         }
     }
    
     class CreditCardPayment implements PaymentProcessor {
         public function processPayment($amount) {
             // Process credit card payment
         }
     }
    

    This way, PaymentProcessor is open for extension (we can add more processors) but closed for modification.


3. Liskov Substitution Principle (LSP)

  • Concept: Objects of a superclass should be replaceable with objects of a subclass without affecting the behavior of the program. In simpler terms, derived classes should be substitutable for their base classes.
  • Why It Matters: Ensuring that subclasses can replace their base classes without issues keeps the program stable and flexible. Violations of LSP can result in unexpected bugs and behavior.

  • Example in PHP:
    Let’s take the PaymentProcessor interface. All classes implementing PaymentProcessor should provide a consistent way to process payments. Here’s an example:

     function makePayment(PaymentProcessor $processor, $amount) {
         $processor->processPayment($amount);
     }
    
     $paypal = new PayPalPayment();
     $creditCard = new CreditCardPayment();
    
     makePayment($paypal, 100);       // Works with PayPal
     makePayment($creditCard, 100);   // Works with Credit Card
    

    Each payment type can substitute for PaymentProcessor without changing the behavior of makePayment.


4. Interface Segregation Principle (ISP)

  • Concept: A class should not be forced to implement interfaces it doesn’t use. This principle encourages creating smaller, more specific interfaces rather than a large, general-purpose interface.
  • Why It Matters: ISP prevents classes from having unnecessary methods that they don’t need, which can lead to bloated code and unnecessary dependencies.

  • Example in PHP:
    Let’s say we initially create a large interface for payments that includes both payment and refund methods:

     interface PaymentProcessor {
         public function processPayment($amount);
         public function processRefund($amount);
     }
    

    If we implement this interface in classes where refunds don’t apply, we are violating ISP. Instead, we can split the interface into two smaller interfaces:

     interface PaymentProcessor {
         public function processPayment($amount);
     }
    
     interface Refundable {
         public function processRefund($amount);
     }
    
     class CreditCardPayment implements PaymentProcessor, Refundable {
         public function processPayment($amount) {
             // Process payment
         }
    
         public function processRefund($amount) {
             // Process refund
         }
     }
    

    Now, only classes that need refunds can implement the Refundable interface, making the code more flexible and focused.


5. Dependency Inversion Principle (DIP)

  • Concept: High-level modules should not depend on low-level modules; both should depend on abstractions. In other words, the specifics of a class’s dependencies should be decoupled and abstracted through interfaces.
  • Why It Matters: By using abstractions, you make the code more modular and flexible, enabling easy changes to dependencies and allowing better testing and maintenance.

  • Example in PHP:
    Suppose we have a PaymentService that depends on PayPalPayment directly:

     class PaymentService {
         private $paypalPayment;
    
         public function __construct(PayPalPayment $paypalPayment) {
             $this->paypalPayment = $paypalPayment;
         }
    
         public function makePayment($amount) {
             $this->paypalPayment->processPayment($amount);
         }
     }
    

    This tightly couples PaymentService to PayPalPayment, which violates DIP. By using an interface, we can make PaymentService more flexible:

     interface PaymentProcessor {
         public function processPayment($amount);
     }
    
     class PaymentService {
         private $processor;
    
         public function __construct(PaymentProcessor $processor) {
             $this->processor = $processor;
         }
    
         public function makePayment($amount) {
             $this->processor->processPayment($amount);
         }
     }
    
     $service = new PaymentService(new PayPalPayment());
     $service->makePayment(150);
    

    Here, PaymentService depends on the PaymentProcessor abstraction, allowing us to switch to any payment method by injecting a different processor, like CreditCardPayment.


Following the SOLID principles helps ensure that the code is organized, flexible, and easy to maintain, resulting in robust and scalable software.

Top comments (0)