Single Responsibility Principle (SRP)
Essence
SRP is the first letter of SOLID: a module should have one reason to change. Robert Martin refines it: a "reason" is one interested actor (a team, a department, a role). If a type is edited for a new screen layout, for an API change and for a new storage format, it has three responsibilities.
What a violation looks like in iOS
The classic Massive View Controller: the controller hits the network, parses JSON, formats dates, configures cells and writes to UserDefaults. Any edit touches everything, and it can only be tested through the UI.
Example of splitting
import Foundation
struct Order: Codable { let id: Int; let total: Decimal }
// Violation: one type loads, formats and persists — three reasons to change.
// Better: three small types with one responsibility each.
protocol OrderRepository { func orders() async throws -> [Order] }
struct OrderFormatter {
func title(for order: Order) -> String {
"Order #\(order.id): \(order.total)"
}
}
@MainActor @Observable
final class OrdersViewModel {
private(set) var titles: [String] = []
private let repository: any OrderRepository
private let formatter = OrderFormatter()
init(repository: any OrderRepository) { self.repository = repository }
func load() async {
let orders = (try? await repository.orders()) ?? []
titles = orders.map(formatter.title(for:))
}
}
OrderRepository is responsible for fetching data, OrderFormatter for presentation, OrdersViewModel for screen state. Each is tested separately and the repository is replaced with a stub.
What the interviewer looks for
- SRP is not "one method per class" but one axis of change. Overdo it and you get dozens of throwaway types; the criterion is who will change this code and why.
- Relation to the other principles: SRP makes OCP easier (fewer reasons to touch a class) and DIP easier (small dependencies are simple to inject through protocols).
- Symptoms of a violation: "And" in a name (
NetworkAndCacheManager), a longimportlist, tests that need half of the app to run.