The SOLID principles are five design principles that help create maintainable, scalable, and testable software.
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- 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.