SOLID principles with Swift
SOLID represents 5 five design principles intended to make object-oriented designs more understandable, flexible, and maintainable.
Let’s discuss one by one.
Every class should have only one responsibility.
Example: UserProfileManager
is a single responsibility class which has getUserProfile
function.
class UserProfileManager {
func getUserProfile(userId: String) -> UserProfile {
// Retrieve user profile from database
}
}
Software entities such as classes, modules, and functions should be open for extension but closed for modification. Or in other words, the behavior of a “module” should be extendable without modifying its source code.
Example: Processing different types of orders.
// Base class representing an order
class Order {
var totalAmount: Double
init(totalAmount: Double) {
self.totalAmount = totalAmount
}
func calculateDiscount() -> Double {
return 0
}
}
// Subclass representing a standard order
class StandardOrder: Order {
override func calculateDiscount() -> Double {
return 0
}
}
// Subclass representing a bulk order
class BulkOrder: Order {
override func calculateDiscount() -> Double {
return totalAmount * 0.1
}
}
/// Example usage
let bulkOrder = BulkOrder(totalAmount: 100)
print("bulkOrder-Discount", bulkOrder.calculateDiscount()) // prints 10
Closed for modification: instead of editing existing code, extend 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 {
return totalAmount - promotionalDiscount
}
}
// Usage
let userWithPromotion = PromotionalOrder(totalAmount: 100, promotionalDiscount: 10)
print("promotion-Discount", userWithPromotion.calculateDiscount()) // prints 90
Functions that use references to base classes must be able to use objects of derived classes without knowing it.
Banking example:
// Base class representing a bank account
class AccountParentClass {
var balance: Double
init(balance: Double) {
self.balance = balance
}
func calculateInterest() -> Double {
fatalError("Method must be overridden by subclasses")
}
}
class CurrentAccount: AccountParentClass {
override func calculateInterest() -> Double {
return 0
}
}
class SavingsAccount: AccountParentClass {
let interestRate: Double
init(balance: Double, interestRate: Double) {
self.interestRate = interestRate
super.init(balance: balance)
}
override func calculateInterest() -> Double {
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) // Interest: 0
printInterest(account: savings) // Interest: 100
Clients should not be forced to depend upon methods they do not use. Instead of one large interface, have smaller, specific ones.
Example: Smart Home environment with lights & cameras.
protocol Switchable {
func turnOn()
func turnOff()
}
protocol VideoCapable {
func startRecording()
func stopRecording()
}
class SmartLight: Switchable {
func turnOn() { print("Light turned on") }
func turnOff() { print("Light turned off") }
}
class Thermostat: Switchable {
func turnOn() { print("Thermostat turned on") }
func turnOff() { print("Thermostat turned off") }
}
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") }
}
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Example: Notification System
// Abstraction
protocol NotificationService {
func sendNotification(message: String, toUser: String)
}
Concrete email implementation:
class EmailNotificationService: NotificationService {
func sendNotification(message: String, toUser: String) {
print("Sending email to \(toUser) with message: \(message)")
}
}
Adding SMS without modifying existing code:
class SMSNotificationService: NotificationService {
func sendNotification(message: String, toUser: String) {
print("Sending SMS to \(toUser) with message: \(message)")
}
}
High-level module depends only on abstraction:
class UserNotificationManager {
let notificationService: NotificationService
init(notificationService: NotificationService) {
self.notificationService = notificationService
}
func notifyUser(message: String, user: String) {
notificationService.sendNotification(message: message, toUser: user)
}
}
Usage:
let emailService = EmailNotificationService()
let userManagerWithEmail = UserNotificationManager(notificationService: emailService)
userManagerWithEmail.notifyUser(message: "Welcome!", user: "user@example.com")
let smsService = SMSNotificationService()
let userManagerWithSMS = UserNotificationManager(notificationService: smsService)
userManagerWithSMS.notifyUser(message: "Your code is 1234", user: "+123456789")