SOLID principles in swift

SOLID represents 5 five design principles intended to make object-oriented designs more understandable, flexible, and maintainable

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

Let’s discuss one by one

1. Single Responsibility Principle

Every class should have only one responsibility
Example: In below example class UserProfileManager is single responsibility class which has getUserProfile function

class UserProfileManager {
    func getUserProfile(userId: String) -> UserProfile {
        // Retrieve user profile from database
    }
}

2.Open/Closed Principle

Software entities such as classes, modules, and functions should be open for extension but closed for modification
Or in other term the behavior of a “module” should be extendable without modifying its source code.
Example: –

Processes different types of orders. Let’s consider a simplified version of an order processing system:

// Base class representing an order
class Order {
    var totalAmount: Double
    
    init(totalAmount: Double) {
        self.totalAmount = totalAmount
    }
    
    func calculateDiscount() -> Double {
        return 0
    }
}
// Now below example which shows open for extentions which uses calculateDiscount
// Subclass representing a standard order
class StandardOrder: Order {
    override func calculateDiscount() -> Double {
        // Standard orders have no discount
        return 0
    }
}

// Subclass representing a bulk order
class BulkOrder: Order {
    override func calculateDiscount() -> Double {
        // Bulk orders receive a 10% discount
        return totalAmount * 0.1
    }
}

/// Example usage
let bulkOrder = BulkOrder(totalAmount: 100)
print("bulkOrder-Discount", bulkOrder.calculateDiscount())// prints 10

Explanation of closed for modification: —
Now, imagine that we need to introduce a new type of order, a “Promotional Order,” which applies a fixed discount amount. Instead of modifying the existing code, we can adhere to the Open/Closed Principle by extending the system’s functionality:

// Subclass representing a promotional order
class PromotionalOrder: Order {
    let promotionalDiscount: Double
    
    init(totalAmount: Double, promotionalDiscount: Double) {
        self.promotionalDiscount = promotionalDiscount
        super.init(totalAmount: totalAmount)
    }
    
    override func calculateDiscount() -> Double {
        // Promotional orders apply a fixed discount amount
        return totalAmount - promotionalDiscount
    }
}

// Usage 
let userWithPromotion = PromotionalOrder(totalAmount: 100, promotionalDiscount: 10)
print("promotion-Discount", userWithPromotion.calculateDiscount())// prints 90

3.Liskov Substitution Principle

Functions that use references to base classes must be able to use objects of derived classes without knowing it.
Let’s understand with banking example: –

//---------Example of Liskov Substitution------------------/
// Base class representing a bank account
class AccountParentClass {
    var balance: Double
    
    init(balance: Double) {
        self.balance = balance
    }
    
    // Method to calculate interest
    func calculateInterest() -> Double {
        fatalError("Method must be overridden by subclasses")
    }
}

// Subclass representing a Current Account
class CurrentAccount: AccountParentClass {
    
    // Subclass able to use parent class function here
    override func calculateInterest() -> Double {
        // current account has no interest
        return 0
    }
}

// Subclass representing a Savings Account
class SavingsAccount: AccountParentClass {
    let interestRate: Double
    
    init(balance: Double, interestRate: Double) {
        self.interestRate = interestRate
        super.init(balance: balance)
    }
    // Subclass able to use parent class function here
    override func calculateInterest() -> Double {
        // Calculate interest based on balance and interest rate
        return balance * interestRate
    }
}

func printInterest(account: AccountParentClass) {
    let interest = account.calculateInterest()
    print("Interest:", interest)
}

let checking = CurrentAccount(balance: 1000)
let savings = SavingsAccount(balance: 2000, interestRate: 0.05)

printInterest(account: checking) // Output: Interest: 0
printInterest(account: savings) // Output: Interest: 100
//---------------------------/

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend upon methods that they do not use. Essentially, rather than having one large interface, you should have several smaller functions, so that clients only need to know about the methods that are of interest to them.
Let’s understand with an example : –
Imagine you are developing a software system for a Smart Home environment. In this system, you have different devices like lights & security cameras. Instead of having a single, monolithic interface for all devices, you segregate the interfaces according to their functionalities.

// Interface for any device that can be turned on and off
protocol Switchable {
    func turnOn()
    func turnOff()
}

// Interface for devices that can capture video
protocol VideoCapable {
    func startRecording()
    func stopRecording()
}

// A class for a smart light, which only needs to be switched on or off
class SmartLight: Switchable {
    func turnOn() {
        print("Light turned on")
    }
    
    func turnOff() {
        print("Light turned off")
    }
}

// A class for a thermostat, which can be turned on/off and temperature adjusted
class Thermostat: Switchable {
    func turnOn() {
        print("Thermostat turned on")
    }
    
    func turnOff() {
        print("Thermostat turned off")
    }
}

// A class for a security camera, which can be turned on/off and start/stop recording
class SecurityCamera: Switchable, VideoCapable {
    func turnOn() {
        print("Camera turned on")
    }
    
    func turnOff() {
        print("Camera turned off")
    }
    
    func startRecording() {
        print("Recording started")
    }
    
    func stopRecording() {
        print("Recording stopped")
    }
}

5. Dependency Inversion principle

The dependency inversion principle is a specific methodology for loosely coupled software modules.
A) High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
B)Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
Let’s understand with an example –

Notification System : Imagine you’re building a system where you need to notify users about certain events, like completing a registration process or receiving a message. Initially, you decide to notify users via email.
By following the Dependency Inversion Principle, our code is now more flexible and easier to maintain. We can add new notification methods without changing the high-level modules that use them.

// First, let's define an abstraction for sending notifications:
// Abstraction
protocol NotificationService {
    func sendNotification(message: String, toUser: String)
}

Next, we implement this protocol with a class that sends notifications via email:

// Concrete implementation for email notification
class EmailNotificationService: NotificationService {
    func sendNotification(message: String, toUser: String) {
        print("Sending email to \(toUser) with message: \(message)")
    }
}

Now, let’s say our application evolves, and we also want to send SMS notifications. Instead of modifying our high-level module (which sends notifications), we can simply create a new class that implements NotificationService:

// Another concrete implementation for SMS notification
class SMSNotificationService: NotificationService {
    func sendNotification(message: String, toUser: String) {
        print("Sending SMS to \(toUser) with message: \(message)")
    }
}

The high-level module that uses the NotificationService doesn’t need to know about the concrete implementation:

class UserNotificationManager {
    let notificationService: NotificationService
    
    init(notificationService: NotificationService) {
        self.notificationService = notificationService
    }
    
    func notifyUser(message: String, user: String) {
        notificationService.sendNotification(message: message, toUser: user)
    }
}

You can see that UserNotificationManager is not directly dependent on EmailNotificationService or SMSNotificationService. It depends on the NotificationService abstraction. This way, if we want to change the notification method (from email to SMS), we can do so easily without needing to alter the UserNotificationManager.

To switch the notification method, we just instantiate UserNotificationManager with a different implementation of NotificationService:

let emailService = EmailNotificationService()
let userManagerWithEmail = UserNotificationManager(notificationService: emailService)
userManagerWithEmail.notifyUser(message: "Welcome to our service!", user: "[email protected]")

let smsService = SMSNotificationService()
let userManagerWithSMS = UserNotificationManager(notificationService: smsService)
userManagerWithSMS.notifyUser(message: "Your code is 1234", user: "+123456789")

Leave a Comment

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