
You Shouldn't Always Use SOLID in Swift
swift solid architecture ios oop
You can’t write every class according to SOLID. You shouldn’t. That’s the first truth.
Why?
SOLID is a set of five principles for object-oriented design. They’re good tools—but bad dogma. Misusing them leads to abstraction soup, unnecessary interfaces, and enough boilerplate to choke your productivity.
Let’s break them down with real Swift code.
🧍 S – Single Responsibility Principle (SRP)
“A class should have one reason to change.”
✅ Smart SRP
class LoginValidator {
func isValid(email: String, password: String) -> Bool {
return email.contains("@") && password.count >= 8
}
}
❌ Overkill SRP
class EmailValidator {}
class PasswordValidator {}
class LoginLogger {}
class LoginController {}
Now you’ve got four files just to log in.
Use SRP when: Logic is complex and responsibilities are real.
Avoid it when: Splitting hurts readability more than it helps.
🔐 O – Open/Closed Principle (OCP)
“Open for extension, closed for modification.”
✅ Swift Example
protocol PaymentMethod {
func pay(amount: Double)
}
class ApplePay: PaymentMethod {
func pay(amount: Double) {
print("Paid \(amount) with Apple Pay")
}
}
class CreditCard: PaymentMethod {
func pay(amount: Double) {
print("Paid \(amount) with Credit Card")
}
}
class CheckoutService {
private let method: PaymentMethod
init(method: PaymentMethod) {
self.method = method
}
func checkout(amount: Double) {
method.pay(amount: amount)
}
}
Add new payment types without touching CheckoutService
.
Use OCP when: You’re building for future change.
Avoid it when: You’re guessing.
🪂 L – Liskov Substitution Principle (LSP)
“Subclasses should be substitutable for their base classes.”
❌ Broken LSP
class Bird {
func fly() {
print("Flying")
}
}
class Ostrich: Bird {
override func fly() {
fatalError("Ostriches can't fly")
}
}
✅ Fix with Composition
protocol CanFly {
func fly()
}
class Sparrow: CanFly {
func fly() { print("Sparrow flying") }
}
Use LSP when: Subclasses are truly interchangeable.
Avoid it when: You’re forcing a relationship just to reuse code.
🧩 I – Interface Segregation Principle (ISP)
“Don’t force clients to depend on interfaces they don’t use.”
✅ Lean Interfaces
protocol Printer {
func printDocument()
}
protocol Scanner {
func scanDocument()
}
class SimplePrinter: Printer {
func printDocument() {}
}
❌ Bloated Interface
protocol OfficeMachine {
func printDocument()
func scanDocument()
func faxDocument()
}
class BudgetPrinter: OfficeMachine {
func printDocument() {}
func scanDocument() { fatalError("Not supported") }
func faxDocument() { fatalError("Not supported") }
}
Use ISP when: Your consumers vary wildly in usage.
Avoid it when: One class owns everything already.
🔌 D – Dependency Inversion Principle (DIP)
“Depend on abstractions, not concretions.”
✅ Swift Protocol-Based DIP
protocol DataStore {
func save(data: String)
}
class LocalStorage: DataStore {
func save(data: String) {
print("Saved: \(data)")
}
}
class ProfileManager {
let store: DataStore
init(store: DataStore) {
self.store = store
}
func updateProfile(name: String) {
store.save(data: name)
}
}
Use DIP when: You need testability and flexibility.
Avoid it when: You’re just mocking one thing that won’t change.
⚖️ So, Why Not Always Use SOLID?
- It adds complexity.
- It creates abstraction overhead.
- It slows down teams who just want to ship.
- It solves problems you don’t have—yet.
✅ Use SOLID When:
- You’re building a large, long-lived system
- You expect constant requirement changes
- You’re collaborating with other developers
❌ Ignore SOLID When:
- You’re prototyping or hacking
- You’re building a small, stable app
- You’re applying it just to feel “enterprisey”
🧽 Summary
Writing every class according to SOLID is like wearing a tuxedo to clean your garage. You’ll look fancy but feel stupid.
Use SOLID when it makes things better.
Ignore it when it makes things worse.