Pour bien appréhender les protocoles, je vous propose que l'on continue à jouer un peu avec dans ce chapitre !
Le point sur les types
Je ne sais pas si vous y avez prêté attention, mais un protocole se déclare un peu comme une énumération, une structure ou une classe.
protocol MonProtocole {}
class MaClasse {}
struct MaStructure {}
enum MonEnumeration {}
Il y a une raison à cela ! Le protocole permet de créer un type, de la même façon que les classes, les structures ou les énumérations. Cela veut dire que vous pouvez écrire cela :
protocol UnProtocole { (...) }
var uneVariable: UnProtocole
var unTableau: [UnProtocole]
func uneFonction(param: UnProtocole) -> UnProtocole { (...) }
La seule différence, c'est que vous ne pouvez pas créer d'instance à partir d'un protocole. En effet, une classe / structure / énumération définit des objets. Vous pouvez donc en créer des instances. Alors que les protocoles définissent seulement des listes d'exigences.
Autrement dit, vous ne pouvez pas écrire :
var uneVariable = UnProtocole()
Euh... Mais alors, ceci ne veut rien dire :
var uneVariable: UnProtocole
Si ! Prenons un exemple avec notre protocole Animal
. Animal
est un protocole, donc il définit un type, donc je peux déclarer une variable de type Animal
.
var unAnimal: Animal
Animal n'est pas un objet, donc je ne peux pas créer une instance d' Animal
. Mais tant mieux, car on a vu qu' Animal
était un concept trop abstrait. En revanche, je peux définir cette variable de type Animal.
Cela signifie qu'elle pourra prendre comme valeur une instance de n'importe quelle classe qui implémente le protocole Animal
. On peut donc écrire ceci :
var unAnimal: Animal
unAnimal = Dog()
unAnimal = Bird()
Mais unAnimal
change de type dans ton exemple ! C'est interdit en Swift !
Eh non ! unAnimal
garde le type Animal,
seulement celui-ci fonctionne avec Dog
et Bird
, car ces deux classes se conforment au protocole Animal
.
On peut alors faire des trucs sympas avec les protocoles, comme ceci :
var monTableauDAnimaux: [Animal] = [Dog(), Bird()]
On a un tableau qui contient des objets qui ne sont pas du même type, mais qui se conforment tous au type Animal
. Lorsque je vais programmer en utilisant mon tableau d'animaux, je vais programmer autour de l'interface définie par mon protocole : je ne me soucie pas de savoir quel type d'animal je manipule. C'est une bien meilleure pratique qui rend mon code plus modulaire !
Des protocoles partout !
Jusqu'à présent, on a parlé uniquement de classes qui se conforment à un protocole, mais sachez que n'importe quel type peut se conformer à un protocole. On peut donc écrire ceci :
protocol MonProtocole {}
class MaClasse: MonProtocole {}
struct MaStructure: MonProtocole {}
enum MonEnumeration: MonProtocole {}
Cela fonctionne exactement de la même manière !
Avant, seules les classes pouvaient partager des comportements, grâce à l'héritage. Mais les structures et les énumérations n'ont pas l'héritage.
Grâce aux protocoles, toutes les structures de données peuvent partager des comportements ! Encore mieux, une classe peut partager des méthodes avec une énumération.
protocol MonProtocole {}
protocol UnAutreProtocole : MonProtocole {}
Plusieurs protocoles
Une même classe / structure / énumération peut se conformer à plusieurs protocoles. En effet, un protocole est simplement une liste d'exigences. De ce fait, on peut combiner ces listes pour obtenir une plus grande liste.
Je vous propose donc que l'on rajoute un deuxième protocole. On va appeler ce protocole Nameable
Et il sera implémenté par toute classe qui veut pouvoir avoir un nom et un prénom :
protocol Nameable {
var firstName: String { get set }
var lastName: String { get set }
func getFullName() -> String
}
Notre classe Dog
va adopter le protocole. Lorsqu'une classe adopte plusieurs protocoles, on sépare ceux-ci par une virgule :
class Dog: Animal, Nameable { (...) }
Ensuite, il suffit de répondre à ses exigences.
class Dog: Animal, Nameable {
var firstName: String = ""
var lastName: String = ""
func getFullName() -> String {
return firstName + " " + lastName
}
// (...)
}
La conformance par l’extension
Nous avons vu qu’on pouvait déclarer la conformance a un protocole de la même manière qu’on déclare l'héritage d’une classe.
Sachez qu’il existe un moyen recommandé pour confirmer une classe ou une structure à un protocole : les extensions.
Ne vous inquiétez pas, ça ne change pas la face du monde, c’est juste “propre” car cela permet de séparer le contenu même de la classe du contenu liée à la conformance :
protocol Nameable {
var firstName: String { get set }
var lastName: String { get set }
func getFullName() -> String
}
class Dog: Animal {
var firstName: String = ""
var lastName: String = ""
}
extension Dog: Nameable {
func getFullName() -> String {
return firstName + " " + lastName
}
}
Mais Dog
implémente toujours les propriétés firstName
et lastName
dans la classe elle-même, là, non ? 🧐
Vous avez raison ! Parce qu’une extension ne peut contenir de variables, ces dernières doivent être la classe elle-même. Néanmoins, la conformance est quand même respectée ici.
En résumé
Un protocole définit un type au même titre qu'une classe / structure / énumération.
Un protocole peut être adopté par une classe, une structure, une énumération ou même un autre protocole.
Une classe / structure / énumération peut se conformer à plusieurs protocoles. Dans ce cas, on sépare les protocoles avec des virgules.
Dans le prochain chapitre, nous passerons à la programmation orientée protocole !