Pour déclarer un protocole, on utilise le mot-clé protocol suivi d'un nom et d'accolades.


protocol Animal {
    func makeSound()
    func move()

    var description: String { get set }
}

En gros, un protocole définit les méthodes et les classes qui les implémentent. Un protocole peut aussi contenir des propriétés. Mais, comme pour les méthodes, le protocole va seulement définir si cette propriété peut être modifiée ou non. Il ne va pas lui donner de valeur.

Un protocole peut aussi contenir des propriétés. Mais, comme pour les méthodes, le protocole va seulement définir si cette propriété peut être modifiée ou non. Il ne va pas lui donner de valeur.

Autrement dit, un protocole, c'est une entité qui déclare : "Si tu veux être ça, il faut que tu fasses ça."

Par exemple, dans notre cas : Si tu veux être un Animal, il faut que tu saches faire les choses suivantes : makeSound , move et donner une valeur à la propriété suivante description.

Pour se conformer au protocole, cela se fait en deux étapes :

  1. Adopter le protocole
  2. Répondre à ses exigences

protocol Animal {
    func makeSound()
    func move()

    var description: String { get set }  // Propriété pouvant être modifiée
}

class Dog: Animal {
    var race = ""

    func fetch() {
        print("Je vais chercher la balle...")
    }

    // On répond aux exigences du protocole
    var description = ""

    func move() {
        print("Je cours !")
    }

    func makeSound() {
        print("Wouf !")
    }
}

class Bird: Animal {
    var color = ""

    // On répond aux exigences du protocole
    var description = ""

    func move() {
        print("Je vole !")
    }

    func makeSound() {
        print("Piou Piou")
    }
}

Le type protocole

Le protocole permet de créer un type, de la même façon que les classes, les structures ou les énumérations.


protocol UnProtocole { (...) }

var uneVariable: UnProtocole
var unTableau: [UnProtocole]
func uneFonction(param: UnProtocole) -> UnProtocole { (...) }

On ne peut 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.

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
unAnimal = Dog()
unAnimal = Bird()

Animal n'est pas un objet, donc je ne peux pas créer une instance d'Animal. En revanche, je peux définir cette variable de type Animal : elle pourra prendre comme valeur une instance de n'importe quelle classe qui implémente le protocole Animal.

Il est aussi possible d'écrire :


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 !

N'importe quel type peut se conformer à un protocole :


protocol MonProtocole {}

class MaClasse: MonProtocole {}

struct MaStructure: MonProtocole {}

enum MonEnumeration: MonProtocole {}

Avant, seulement 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.

Se conformer à plusieurs protocoles

Un protocole peut se conformer à un autre protocole :


protocol MonProtocole {}

protocol UnAutreProtocole : MonProtocole {}

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.


protocol Nameable {
    var firstName: String { get set }
    var lastName: String { get set }

    func getFullName() -> String
}

Il suffit ensuite de répondre aux exigences des différents protocoles :


class Dog: Animal, Nameable {
    var firstName: String = ""
    var lastName: String = ""

    func getFullName() -> String {
        return firstName + " " + lastName
    }

    // (...)
}

POP : Protocol Oriented Programming

En Swift, un protocole peut définir l'implémentation de ses méthodes.

Voici la définition du protocole que l'on avait :


protocol Nameable {
    var firstName: String { get set }
    var lastName: String { get set }

    func getFullName() -> String
}

Et pour implémenter getFullName, on utilise une extension de protocole :


extension Nameable {

    func getFullName() -> String {
      return firstName + " " + lastName
    }
}

Donc pour que Human adopte Nameable :


class Human: Nameable {
    var firstName: String = ""
    var lastName: String = ""

    // Pas besoin d'implémenter getFullName, mais toute instance 
       de Human pourra l'appeler !!

    func speak() {
        print("Bonjour !")
    }
}

On peux alors facilement ajouter une brique de fonctionnalité bien définie à n'importe quelle classe/structure/énumération !

On ne peut pas utiliser override dans ce genre de situation. Si, dans ma classe Dog, je veux une implémentation de getFullName différente de celle par défaut, je n'ai qu'à la redonner :


class Dog: Nameable {
    var firstName: String = ""
    var lastName: String = ""

    // Lorsque j'appelle cette méthode sur un chien,
    // c'est cette implémentation qui sera appellée,
    // et non celle définie par défaut.
    func getFullName() -> String {
      return "Waaaf \(firstName) ! WoofWoof \(lastName)"
    }

    // (...)
}

En POO (orienté object)

En orienté objet, on partage des comportements grâce à l'héritage. Mais on est limité à une seule superclasse et toutes les classes récupèrent forcément toutes les fonctionnalités de la superclasse. Cela oblige les sous-classes à une certaine homogénéité.

En orienté protocole, on partage des comportements grâce aux protocoles. C'est bien plus flexible, car :

  • On peut partager des comportements entre des classes/structures/énumérations
  • Une même classe/structure/énumération peut adopter plusieurs protocoles
  • En utilisant un protocole par fonctionnalité, on peut ajouter certaines fonctionnalités à certains objets sans les ajouter à d'autres
  • On peut donner une implémentation par défaut de certaines fonctionnalités en utilisant les extensions de protocole

L'orienté protocole est au coeur de la conception de Swift. Le langage lui-même utilise de nombreux protocoles pour fonctionner.