Einleitung: Warum SOLID für Swift-Entwickler wichtig ist
Jeder iOS-Entwickler stößt früher oder später auf das Problem von "Spaghetti-Code": Controller schwellen auf Tausende von Zeilen an, die Änderung einer Entität zerstört drei andere, und das Testen wird zum Albtraum. Die SOLID-Prinzipien sind fünf grundlegende Regeln des objektorientierten Designs, die helfen, flexiblen, wartbaren und testbaren Code in Swift zu erstellen.
In diesem Artikel werden wir jedes SOLID-Prinzip anhand konkreter Beispiele aus der iOS-Entwicklung analysieren. Sie erfahren, wie Sie diese Prinzipien in realen Projekten anwenden und typische Fehler vermeiden. Das Material ist sowohl für Junior-Entwickler als auch für erfahrene Fachleute nützlich, die ihr Wissen systematisieren möchten.
1. Single Responsibility Principle (SRP) — Prinzip der einzigen Verantwortung
Kernaussage: Jede Klasse sollte nur einen Grund für eine Änderung haben. Mit anderen Worten, eine Klasse sollte nur für eine Aufgabe verantwortlich sein.
Problem: Der "göttliche" ViewController
Ein typischer Fehler ist es, die gesamte Logik in den UIViewController zu packen. Betrachten wir ein schlechtes Beispiel:
class UserProfileViewController: UIViewController { @IBOutlet weak var nameLabel: UILabel! func loadUser() { // 1. Netzwerklogik let url = URL(string: "https://api.example.com/user")! URLSession.shared.dataTask(with: url) { data, _, _ in // 2. JSON-Parsing let user = try! JSONDecoder().decode(User.self, from: data!) // 3. Speichern in Core Data self.saveToCoreData(user) // 4. UI-Update DispatchQueue.main.async { self.nameLabel.text = user.name } }.resume() } private func saveToCoreData(_ user: User) { /* ... */ }}Lösung: Aufteilung der Verantwortung
Teilen wir den Code in separate Komponenten auf:
// 1. Service für die Netzwerkarbeitclass UserNetworkService { func fetchUser(completion: @escaping (Result<User, Error>) -> Void) { let url = URL(string: "https://api.example.com/user")! URLSession.shared.dataTask(with: url) { data, _, error in if let data = data { do { let user = try JSONDecoder().decode(User.self, from: data) completion(.success(user)) } catch { completion(.failure(error)) } } else if let error = error { completion(.failure(error)) } }.resume() }}
// 2. Datenverwaltungs-Managerclass UserStorageManager { func save(_ user: User) { // Logik zum Speichern in Core Data / Realm }}
// 3. ViewModel (oder Presenter)class UserViewModel { private let networkService = UserNetworkService() private let storageManager = UserStorageManager() var onUserUpdated: ((User) -> Void)? func loadUser() { networkService.fetchUser { [weak self] result in switch result { case .success(let user): self?.storageManager.save(user) self?.onUserUpdated?(user) case .failure(let error): print(error.localizedDescription) } } }}
// 4. ViewController verwaltet jetzt nur noch die UIclass UserProfileViewController: UIViewController { @IBOutlet weak var nameLabel: UILabel! private let viewModel = UserViewModel() override func viewDidLoad() { super.viewDidLoad() viewModel.onUserUpdated = { [weak self] user in DispatchQueue.main.async { self?.nameLabel.text = user.name } } viewModel.loadUser() }}Ergebnis: Jede Klasse hat einen Grund für eine Änderung. Das Testen und Warten solchen Codes ist deutlich