Introduction aux principes SOLID en Swift
\nTout développeur iOS rencontre tôt ou tard le problème du « code spaghetti » : les classes gonflent, les modifications à un endroit cassent toute l'application, et les tests deviennent un cauchemar. La solution à ces problèmes réside dans la conception orientée objet — les principes SOLID. Ces cinq règles, formulées par Robert Martin, aident à créer un code flexible, maintenable et évolutif en Swift. Dans cet article, nous allons détailler chaque principe, de la théorie aux exemples pratiques pour iOS/macOS.
\nQue vous écriviez en UIKit, SwiftUI ou AppKit, SOLID est une base universelle. Plongeons dans les détails.
\n\n1. Single Responsibility Principle (SRP) — Principe de responsabilité unique
\nThéorie
\nUne classe ne doit avoir qu'une seule raison de changer. Autrement dit, chaque module est responsable d'exactement une fonctionnalité. Si votre ViewController gère l'interface utilisateur, traite les requêtes réseau et parse le JSON, il viole le SRP.
\nPratique en Swift
\nConsidérons un exemple typique — le chargement et l'affichage des données d'un utilisateur.
\n// ❌ Mauvais : violation du SRP\nclass UserViewController: UIViewController {\n func loadUser() {\n // Requête réseau\n let url = URL(string: "https://api.example.com/user")!\n URLSession.shared.dataTask(with: url) { data, _, _ in\n let user = try? JSONDecoder().decode(User.self, from: data!)\n DispatchQueue.main.async {\n self.updateUI(with: user)\n }\n }.resume()\n }\n \n func updateUI(with user: User?) {\n // Mise à jour de l'interface\n }\n}\n// ✅ Bon : séparation des responsabilités\nclass UserService {\n func fetchUser(completion: @escaping (User?) -> Void) {\n let url = URL(string: "https://api.example.com/user")!\n URLSession.shared.dataTask(with: url) { data, _, _ in\n let user = try? JSONDecoder().decode(User.self, from: data!)\n completion(user)\n }.resume()\n }\n}\n\nclass UserViewController: UIViewController {\n let userService = UserService()\n \n func loadUser() {\n userService.fetchUser { [weak self] user in\n DispatchQueue.main.async {\n self?.updateUI(with: user)\n }\n }\n }\n \n private func updateUI(with user: User?) {\n // Mise à jour de l'interface\n }\n}\nConclusion : Désormais, UserService est uniquement responsable du réseau, et UserViewController de l'interface utilisateur. Les modifications dans la logique réseau n'affectent pas le contrôleur.
2. Open-Closed Principle (OCP) — Principe d'ouverture/fermeture
\nThéorie
\nLes classes doivent être ouvertes à l'extension, mais fermées à la modification. Au lieu de modifier le code existant, nous ajoutons du nouveau via l'héritage ou les protocoles. En Swift, l'outil idéal pour cela sont les protocoles et les extensions.
\nPratique en Swift
\nImaginez que nous écrivions un module pour calculer des réductions dans une boutique en ligne.
\n// ❌ Mauvais : lors de l'ajout d'une nouvelle réduction, il faut modifier la classe\nclass DiscountCalculator {\n func calculate(price: Double, type: String) -> Double {\n if type == "seasonal" {\n return price * 0.9\n } else if type == "loyalty" {\n return price * 0.85\n }\n return price\n }\n}\n// ✅ Bon : utilisation d'un protocole pour l'extension\nprotocol DiscountStrategy {\n func applyDiscount(to price: Double) -> Double\n}\n\nstruct SeasonalDiscount: DiscountStrategy {\n func applyDiscount(to price: Double) -> Double {\n price * 0.9\n }\n}\n\nstruct LoyaltyDiscount: DiscountStrategy {\n func applyDiscount(to price: Double) -> Double {\n price * 0.85\n }\n}\n\nclass DiscountCalculator {\n func calculate(price: Double, strategy: DiscountStrategy) -> Double {\n strategy.