Fiche 00.04 — Protocols et protocol-oriented programming
Objectif
Maîtriser le protocol comme contrat et comme type, les protocol extensions (le cœur du POP), et l'usage des protocols pour l'injection de dépendances et la testabilité. C'est la base qui rend tes services mockables et tes ViewModels testables sans réseau.
1. Protocol = contrat
Un protocol décrit ce qu'un type doit savoir faire, sans dire comment. Un type qui le respecte est dit conforme.
Pont UIKit : tu connais déjà ça avec UITableViewDataSource. Quand tu écrivais class MyVC: UIViewController, UITableViewDataSource, tu conformais ta classe au protocole, et le compilateur t'obligeait à implémenter tableView(_:numberOfRowsInSection:). Même mécanique ici, mais on va s'en servir bien au-delà de la délégation.
2. Protocol comme type (abstraction)
Un protocol n'est pas qu'une checklist : c'est aussi un type que tu peux utiliser pour des paramètres, des propriétés, des retours. Tu manipules l'abstraction, pas le type concret.
any Shape = une boîte qui contient un Shape quelconque (existential, résolu au runtime). À distinguer de some Shape (opaque, un seul type concret connu à la compilation) — c'est exactement le some View que tu vois partout en SwiftUI.
3. Protocol extensions = le cœur du POP
C'est LA différence avec UIKit. Tu peux donner une implémentation par défaut à une méthode du protocole via une extension. Les types conformes en héritent gratuitement et peuvent surcharger.
C'est ce qu'on appelle le protocol-oriented programming : composer du comportement par protocols + extensions plutôt que par héritage de classes. En UIKit tu factorisais via une UIViewController de base ; ici tu n'as pas besoin de classe parente.
Piège classique du dispatch statique :
Règle : si une méthode doit être polymorphe via le protocol, déclare-la dans le protocol, pas seulement dans l'extension.
4. Associated types et contraintes (bref)
Un associatedtype est un type générique « à trous » dans le protocol, résolu par le type conforme. C'est le mécanisme derrière Collection, Identifiable (son ID), etc.
Tu peux contraindre un générique à un protocol (where), ou contraindre l'associatedtype :
Bon à savoir : depuis Swift 5.7, tu peux écrire any Container même avec associated types (existentials généralisés). À garder simple tant que tu n'en as pas besoin.
5. Protocols pour la DI et la testabilité (l'usage central)
C'est ici que les protocols paient en pratique. Tu définis un service par son interface (un protocol), pas par son implémentation. Le ViewModel dépend de l'abstraction → tu peux injecter une vraie implémentation en prod et un mock en test.
Note Swift moderne : @Observable (macro Observation, iOS 17+) remplace ObservableObject / @Published. Le ViewModel reste testable parce qu'il dépend d'AnimalServicing, jamais de URLSession directement.
Le mock pour les tests :
Pas de réseau, pas de flakiness, test instantané. C'est le pattern DI repris en détail dans la Partie 06.
Pont UIKit : un delegate est un protocol, oui — mais c'est de la délégation (notifier un objet d'événements). Ici on utilise le protocol pour de l'injection de dépendances : remplacer une brique entière (le service) par une autre. Même outil, intention différente.
6. Les protocols standard à connaître
Ils ne sont pas décoratifs : SwiftUI et la stdlib s'appuient dessus pour fonctionner.
| Protocol | À quoi ça sert | Où ça compte |
|---|---|---|
Identifiable | fournit un id stable | List, ForEach sans id:, diffing/animations |
Equatable | comparer avec == | tests, .onChange, Set, optimisation des re-renders |
Hashable | utilisable comme clé / dans un Set | Set, Dictionary, NavigationStack paths, id de ForEach |
Codable | encode/décode JSON | réseau, persistance (Codable = Encodable & Decodable) |
Comparable | tri (<, sorted()) | sorted(), ranges |
Le compilateur synthétise Equatable, Hashable et Codable automatiquement si toutes les propriétés sont elles-mêmes conformes. Tu déclares juste la conformance.
Pourquoi Identifiable change tout pour List :
7. Composer plusieurs protocols
Un type peut être conforme à plusieurs protocols, et tu peux exiger une combinaison.
C'est aussi pour ça que Codable existe : c'est un simple alias typealias Codable = Encodable & Decodable.
Points à connaître
- Méthode dans l'extension uniquement ≠ polymorphe. Si elle n'est pas déclarée dans le protocol, l'appel via le type protocol part en dispatch statique (cf. section 3). Déclare-la dans le protocol si tu veux la surcharge.
anya un coût,somenon.any Shape= existential (indirection runtime) ;some Shape= type opaque résolu à la compilation. En hot path préfère les génériques /some.- Ne mocke pas en sous-classant ta vraie classe. Extrais un protocol (
AnimalServicing) et fais un struct mock conforme. Plus propre, pas besoin d'open/override, et ça marche avec desstruct. - N'oublie pas
Identifiablesur tes modèles de liste.id: \.selfest un signe que ton modèle devrait êtreIdentifiable.
Exercice
Pars de ce service concret :
- Extrais un protocol
WeatherProvidingavec la méthodetemperature(for:). - Fais conformer
WeatherServiceau protocol (sans changer son corps). - Écris un
MockWeatherProviderqui renvoie une valeur fixe que tu passes à l'init. - Bonus : écris un
@Testqui vérifie qu'un ViewModel@Observablerecevant le mock affiche bien la température.
Objectif : 10-20 min, aucun appel réseau réel.
Question d'entretien
Q : Pourquoi utiliser des protocols pour tester ?
R : Pour casser la dépendance à des briques lentes ou non déterministes (réseau, base de données, horloge). Le code dépend d'une abstraction (AnimalServicing) injectée ; en test j'injecte un mock qui renvoie des résultats contrôlés, succès ou erreur. Tests rapides, déterministes, sans I/O réel — c'est de l'inversion de dépendance (le « D » de SOLID).
Q : Quelle différence entre some View et any View ?
R : some View est un type opaque : un seul type concret, connu à la compilation, sans coût d'indirection — c'est ce qu'on retourne d'un body. any View est un existential : il peut contenir des types différents au runtime, au prix d'une indirection. Pour body on veut some.
Q : C'est quoi le protocol-oriented programming face à l'héritage de classes ?
R : Composer du comportement via des protocols + protocol extensions (implémentations par défaut) plutôt que via une hiérarchie de classes. Ça marche avec les struct (value types), évite les problèmes d'héritage multiple/fragile, et permet de mixer des capacités à la carte. Apple pousse cette approche depuis « Protocol-Oriented Programming in Swift » (WWDC 2015).
Résumé
- Un protocol est à la fois un contrat (ce qu'un type sait faire) et un type (
any/some). - Les protocol extensions donnent des implémentations par défaut : c'est le cœur du POP, et ça remplace souvent l'héritage de classes.
- Une méthode définie uniquement dans une extension n'est pas polymorphe via le type protocol (dispatch statique).
- L'usage clé en pratique : DI + testabilité. On dépend d'un protocol service (
AnimalServicing), on injecte la vraie implémentation en prod et un mock en test. Identifiable,Equatable,Hashable,Codablesont des protocols standard que SwiftUI/stdlib utilisent pourList,Set, le diffing et le JSON — souvent synthétisés par le compilateur.- Pont UIKit : un
delegateest un protocol (délégation) ; ici on se sert des protocols surtout pour l'injection de dépendances.