Introduction: Why SOLID is Important for a Swift Developer
Developing for iOS and macOS requires not only knowledge of frameworks but also the ability to create a maintainable, testable, and scalable architecture. This is where SOLID principles come to the rescue — five fundamental rules of object-oriented design formulated by Robert C. Martin (Uncle Bob).
In this article, we will break down each of the five SOLID principles using Swift code examples, show typical antipatterns, and suggest correct solutions. After reading, you will be able to write code that is easy to maintain, extend, and test.
1. Single Responsibility Principle (SRP)
Statement: A class should have only one reason to change. In other words, each module is responsible for exactly one functionality.
Problem: The God Class
Consider a typical example from iOS development — a UserManager class that does too much:
class UserManager { func fetchUser() -> User { /* network request */ } func saveUserToDatabase(_ user: User) { /* working with CoreData */ } func formatUserDisplayName(_ user: User) -> String { /* formatting */ } func sendWelcomeEmail(to user: User) { /* sending email */ }}Here, the class is responsible for network requests, the database, UI formatting, and sending emails. Changing any of these aspects would lead to modifying UserManager.
Solution: Separation of Responsibilities
class UserNetworkService { func fetchUser() -> User { /* only network request */ }}
class UserDatabaseService { func saveUser(_ user: User) { /* only working with DB */ }}
class UserFormatter { func displayName(for user: User) -> String { /* only formatting */ }}
class EmailService { func sendWelcome(to user: User) { /* only sending email */ }}Now each class has one clear responsibility, which simplifies testing and modification.
2. Open/Closed Principle (OCP)
Statement: Software entities should be open for extension but closed for modification.
Problem: Rigid Logic
Suppose we have a class for calculating discounts:
class DiscountCalculator { func calculateDiscount(for order: Order, customerType: String) -> Double { if customerType == "regular" { return order.total * 0.05 } else if customerType == "premium" { return order.total * 0.1 } else if customerType == "vip" { return order.total * 0.2 } return 0 }}When adding a new customer type (gold), you would have to modify the existing class, which violates OCP.
Solution: Protocols and Polymorphism
protocol DiscountStrategy { func discount(for order: Order) -> Double}
struct RegularDiscount: DiscountStrategy { func discount(for order: Order) -> Double { order.total * 0.05 }}
struct PremiumDiscount: DiscountStrategy { func discount(for order: Order) -> Double { order.total * 0.1 }}
struct VIPDiscount: DiscountStrategy { func discount(for order: Order) -> Double { order.total * 0.2 }}
class DiscountCalculator { func calculateDiscount(for order: Order, using strategy: DiscountStrategy) -> Double { return strategy.discount(for: order) }}Now, to add a new discount, you simply create a new structure implementing the DiscountStrategy protocol, without modifying DiscountCalculator.
3. Liskov Substitution Principle (LSP)
Statement: