
SOLID Principles in Swift (iOS)
When writing iOS applications, maintaining clean, scalable, and testable code is crucial. The SOLID principles help us achieve this by structuring our code in a way that makes it easier to manage and extend over time. Let’s dive into these principles with engaging real-world examples in Swift!
1. Single Responsibility Principle (SRP)
A class should have only one reason to change.
The SRP emphasizes that a class should focus on a single task or responsibility. By adhering to this principle, we ensure that each class is more maintainable and less prone to errors when requirements evolve.
Real-world Example: Hospital Management System
A hospital system should separate different functionalities such as patient registration, billing, and medical record management.
Wrong Approach:
class HospitalManagement {
func registerPatient() {
print("Patient registered successfully")
}
func generateBill() {
print("Bill generated")
}
func updateRecord() {
print("Medical record updated")
}
}
Issues: This class handles multiple responsibilities, making it hard to maintain and modify.
Refactored:
class PatientRegistration {
func registerPatient() {
print("Patient registered successfully")
}
}
class BillingSystem {
func generateBill() {
print("Bill generated")
}
}
class MedicalRecords {
func updateRecord() {
print("Medical record updated")
}
}
Each class now has a single responsibility, making the system modular and easier to maintain.
2. Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
OCP advocates that existing code should not be modified when adding new functionality. Instead, extend the behavior by adding new code.
Real-world Example: Payment Processing
A payment system should allow adding new payment methods without modifying existing code.
Wrong Approach:
class PaymentProcessor {
func processCreditCard() {
print("Processing credit card payment")
}
func processPayPal() {
print("Processing PayPal payment")
}
}
Issues: Adding a new payment method requires modifying the class, violating OCP.
Refactored:
protocol PaymentMethod {
func processPayment()
}
class CreditCardPayment: PaymentMethod {
func processPayment() {
print("Processing credit card payment")
}
}
class PayPalPayment: PaymentMethod {
func processPayment() {
print("Processing PayPal payment")
}
}
class PaymentProcessor {
func process(payment: PaymentMethod) {
payment.processPayment()
}
}
Adding a new payment method (e.g., Apple Pay) requires creating a new class implementing PaymentMethod
, without modifying existing code.
3. Liskov Substitution Principle (LSP)
Subtypes should be substitutable for their base types without altering behavior.
LSP ensures that derived classes can stand in for their base classes without causing errors.
Real-world Example: Vehicle Rental System
A rental system should differentiate between electric and non-electric vehicles without breaking the base class.
Wrong Approach:
class Vehicle {
func startEngine() {
print("Vehicle engine started")
}
}
class ElectricCar: Vehicle {
override func startEngine() {
fatalError("Electric cars don't have engines!")
}
}
Issues: The subclass violates LSP by altering the expected behavior.
Refactored:
protocol Vehicle {
func startEngine()
}
class Car: Vehicle {
func startEngine() {
print("Car engine started")
}
}
class ElectricCar: Vehicle {
func startEngine() {
print("Electric car powered on")
}
}
Both Car
and ElectricCar
conform to Vehicle
, ensuring substitutability without errors.
4. Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
ISP suggests creating specific interfaces to avoid forcing classes to implement unnecessary methods.
Real-world Example: Restaurant Ordering System
A restaurant system should not force a self-service kiosk to implement waiter-specific functionalities.
Wrong Approach:
protocol RestaurantService {
func takeOrder()
func serveFood()
}
class Kiosk: RestaurantService {
func takeOrder() {
print("Processing self-service order")
}
func serveFood() {
fatalError("Kiosk cannot serve food!")
}
}
Issues: The kiosk is forced to implement serveFood()
, which it doesn’t support.
✅ Refactored:
protocol OrderTaking {
func takeOrder()
}
protocol TableService {
func serveFood()
}
class Waiter: OrderTaking, TableService {
func takeOrder() {
print("Taking order from customer")
}
func serveFood() {
print("Serving food at the table")
}
}
class Kiosk: OrderTaking {
func takeOrder() {
print("Processing self-service order")
}
}
Now, Kiosk
is not forced to implement serveFood()
, making the system more flexible.
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
DIP promotes decoupling by ensuring that both high-level and low-level modules depend on abstractions, not on concrete implementations.
Real-world Example: Logistics System
A logistics system should support multiple delivery providers (DHL, FedEx) without tight coupling.
Wrong Approach:
class OrderManager {
let dhl = DHLService()
func shipOrder() {
dhl.deliverPackage()
}
}
Issues: The OrderManager
is tightly coupled with DHLService
, making it hard to switch providers.
Refactored:
protocol DeliveryService {
func deliverPackage()
}
class DHLService: DeliveryService {
func deliverPackage() {
print("Package delivered by DHL")
}
}
class FedExService: DeliveryService {
func deliverPackage() {
print("Package delivered by FedEx")
}
}
class OrderManager {
let deliveryService: DeliveryService
init(deliveryService: DeliveryService) {
self.deliveryService = deliveryService
}
func shipOrder() {
deliveryService.deliverPackage()
}
}
Now, OrderManager
can work with any DeliveryService
, making it easy to switch providers.
Wrapping Up
Applying SOLID principles in Swift leads to cleaner, more scalable, and maintainable code. Start implementing these in your projects and experience the difference!