SOLID Principle

The SOLID principles are five design principles that help create maintainable, scalable, and testable software.

  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

1. Single Responsibility Principle (SRP)

A class should have only one reason to change (i.e only one responsibility).

Example (Violating SRP)

class UserManager {
    func saveUserData() {
        print("Saving user data to database")
    }
    
    func sendEmailVerification() {
        print("Sending verification email")
    }
}

Example (Following SRP)

class UserDataManager {
    func saveUserData() {
        print("Saving user data to database")
    }
}

class EmailService {
    func sendEmailVerification() {
        print("Sending verification email")
    }
}

// Usage
let userManager = UserDataManager()
userManager.saveUserData()

let emailService = EmailService()
emailService.sendEmailVerification()

2.Open/Closed Principle

A class should be open for extension but closed for modification.

Example (Violating OCP)

class Payment {
    func processPayment(type: String) {
        if type == "CreditCard" {
            print("Processing Credit Card payment")
        } else if type == "UPI" {
            print("Processing UPI payment")
        }
    }
}
  • Every time a new payment method is introduced, we need to modify this class.
  • This violates OCP because the class is not closed for modification.

Example (Following OCP)

We refactor the design to follow OCP using protocols and polymorphism.

// Step 1: Create a Payment protocol
protocol Payment {
    func processPayment()
}

// Step 2: Implement different payment methods
class CreditCardPayment: Payment {
    func processPayment() {
        print("Processing Credit Card payment")
    }
}

class UPIPayment: Payment {
    func processPayment() {
        print("Processing UPI payment")
    }
}

class PayPalPayment: Payment {
    func processPayment() {
        print("Processing PayPal payment")
    }
}

// Step 3: Used Dependency Injection in the PaymentProcessor class
class PaymentProcessor {
    func process(payment: Payment) {
        payment.processPayment()
    }
}

// Usage
let paymentProcessor = PaymentProcessor()
let creditCardPayment = CreditCardPayment()
let upiPayment = UPIPayment()
let paypalPayment = PayPalPayment()

paymentProcessor.process(payment: creditCardPayment)  // Output: Processing Credit Card payment
paymentProcessor.process(payment: upiPayment)         // Output: Processing UPI payment
paymentProcessor.process(payment: paypalPayment)      // Output: Processing PayPal payment
  • No need to PaymentProcessor when adding a new payment method.
  • New payment types can be added without changing existing code.
  • The system is open for extension but closed for modification.

3.Liskov Substitution Principle (LSP)

Subclasses should be able to replace their base classes without breaking the application. Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

Example (Violating LSP)

class Bird {
    func fly() {
        print("Flying...")
    }
}

class Penguin: Bird {
    override func fly() {
        fatalError("Penguins can't fly!")
    }
}

Example (Following LSP)

protocol Bird {
    func move()
}

class Sparrow: Bird {
    func move() {
        print("Flying...")
    }
}

class Penguin: Bird {
    func move() {
        print("Swimming...")
    }
}

4.Interface Segregation Principle (ISP)

A class should not be forced to implement methods it doesn’t need.

Example (Violating ISP)

protocol Worker {
    func work()
    func eat()
}

class Robot: Worker {
    func work() {
        print("Working...")
    }

    func eat() {
        // not needed 
    }
}

Example (Following ISP)

protocol Workable {
    func work()
}

protocol Eatable {
    func eat()
}

class Human: Workable, Eatable {
    func work() {
        print("Working...")
    }

    func eat() {
        print("Eating...")
    }
}

class Robot: Workable {
    func work() {
        print("Working...")
    }
}

Now, Robot only implements what it needs.

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules; both should depend on abstractions.

Example (Violating DIP)

class LightBulb {
    func turnOn() {
        print("Light is ON")
    }

    func turnOff() {
        print("Light is OFF")
    }
}

class Switch {
    let bulb = LightBulb() // Direct dependency on a concrete class

    func operate() {
        bulb.turnOn()
    }
}

The Switch class depends on LightBulb, making it hard to switch to LED, Smart Bulbs.

Example (Following DIP)

protocol Switchable {
    func turnOn()
    func turnOff()
}

class LightBulb: Switchable {
    func turnOn() {
        print("Light is ON")
    }

    func turnOff() {
        print("Light is OFF")
    }
}

class SmartLight: Switchable {
    func turnOn() {
        print("Smart Light is ON")
    }

    func turnOff() {
        print("Smart Light is OFF")
    }
}

class Switch {
    let device: Switchable

    init(device: Switchable) {
        self.device = device
    }

    func operate() {
        device.turnOn()
    }
}

SRP: Keeps code organized and maintainable.

OCP: Makes it easy to extend functionality.

LSP: Prevents unexpected behavior in subclasses.

ISP: Keeps interfaces clean and specific.

DIP: Makes code loosely coupled and more flexible.

Leave a Reply

Your email address will not be published. Required fields are marked *