ProgrammationSwift Middle/Lead

Qu'est-ce que la composition de protocoles en Swift, comment ça fonctionne dans la pratique et quand devrait-on l'appliquer plutôt que l'héritage multiple ou l'utilisation habituelle d'un protocole ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

La composition de protocoles est un mécanisme en Swift qui permet de créer un type qui doit se conformer à plusieurs protocoles en même temps. C'est un moyen alternatif de remplacer l'héritage multiple, qui n'existe pas pour les classes en Swift.

Historique de la question

Objective-C supportait l'héritage multiple uniquement pour les protocoles, mais pas pour les classes. Swift développe cette tradition en mettant l'accent sur les protocoles et leur combinaison pour construire de nouvelles abstractions.

Problème

Les programmeurs sont souvent confrontés à la tâche de créer un type dont le comportement est défini par plusieurs abstractions. L'héritage multiple entraîne inévitablement des conflits d'hierarchies, et en Swift, cela est résolu de manière sécurisée grâce aux protocoles et à la composition de protocoles.

Solution

En Swift, il est possible de combiner des protocoles à l'aide de l'opérateur '&'. Cela permet de créer des variables ou des paramètres de fonction qui doivent se conformer à plusieurs protocoles simultanément.

Exemple de code :

protocol Drivable { func drive() } protocol Flyable { func fly() } struct FlyingCar: Drivable, Flyable { func drive() { print("Driving") } func fly() { print("Flying") } } func testVehicle(_ vehicle: Drivable & Flyable) { vehicle.drive() vehicle.fly() } testVehicle(FlyingCar())

Caractéristiques clés :

  • L'opérateur '&' permet de combiner plusieurs protocoles pour une variable ou un paramètre de fonction.
  • Cela fonctionne à la fois pour les classes, les structures et les énumérations.
  • Cela permet de décrire explicitement le comportement requis sans créer de types intermédiaires.

Questions pièges.

Peut-on passer un objet qui implémente seulement un des protocoles en paramètre d'une composition de protocoles ?

Non, l'objet doit implémenter tous les protocoles impliqués dans la composition, sinon le compileur renverra une erreur.

Exemple de code :

// struct Car: Drivable {} — ne peut pas être passé à testVehicle car fly() n'est pas implémenté

La composition de protocoles fonctionne-t-elle avec des types, et pas seulement avec des valeurs ?

La composition de protocoles s'applique aux valeurs (variables, paramètres de fonctions), mais n'est pas utilisée lors de la définition du type d'un objet (par exemple, on ne peut pas déclarer un nouveau type comme une "composition" de protocoles — seulement une variable).

Exemple de code :

var obj: SomeProtocol & AnotherProtocol // permis // typealias MyType = SomeClass & AnotherProtocol // erreur

Peut-on combiner des classes et des protocoles via '&' ?

Oui, mais avec une seule restriction : un seul type de classe (classe ou son successeur) peut être à gauche, les autres ne peuvent être que des protocoles, sinon le compileur renverra une erreur.

Exemple de code :

class A {} protocol B {} // func f(obj: A & B) {} // permis // func f(obj: A & AnotherClass & B) {} // erreur ! Un seul type de classe est autorisé

Erreurs typiques et anti-patterns

  • Attendre un fonctionnement à la manière de l'héritage multiple des classes.
  • Utiliser la composition sans nécessité, alors qu'un seul protocole serait suffisant.
  • Violations des contrats lors de l'utilisation de bibliothèques externes et de la composition de protocoles.

Exemple de la vie réelle

Exemple négatif

Dans un projet, pour le transfert de données entre couches, des objets avec une composition de protocoles sont utilisés, alors qu'un seul protocole aurait suffi :

func present(item: Displayable & Serializable) { ... }

Avantages :

  • Interface flexible et extensible. Inconvénients :
  • Complexité et confusion si la plupart des objets transmis ne nécessitent pas toutes les fonctionnalités.

Exemple positif

Utilisation de la composition de protocoles uniquement dans des cas explicites — par exemple, traitement d'objets qui supportent à la fois Codable et Identifiable pour la sérialisation générique :

func save<T: Codable & Identifiable>(_ item: T) { ... }

Avantages :

  • Description claire des exigences sur le type.
  • Minimisation des erreurs. Inconvénients :
  • Peut légèrement compliquer la signature des fonctions.