1

POO : Les protocoles

Cet article n'a pas été mis à jour depuis plus d'un an, il est possible que certaines informations ne soient plus à jour. Si vous rencontrez des erreurs ou des différences en le suivant, n'hésitez pas à commenter pour me le signaler.

Et bonjour à tous ! On se retrouve ensemble aujourd’hui pour parler un peu plus de la POO en s’attaquant aux protocoles de classe et je sens qu’on va bien s’amuser. Je recommande chaudement d’avoir d’abord lu les autres tutoriels traitant de la POO si vous daignez comprendre un mot de celui-ci. On va parler type et héritage, alors installez vous confortablement devant votre clavier et c’est parti !

 

Une (petite) partie des protocoles de communication dans le monde… ça n’a rien à voir !

De quoi ça s’agit

Pour savoir ce qu’est un protocole je vais tricher et m’appuyer sur la définition de ce cher wikipedia qui nous apprends qu’un protocole, ce sont les conventions qui facilitent une communication sans faire directement partie du sujet de la communication elle-même. Et il s’agit exactement de ça, les différentes classes de notre code vont communiquer entre elles et s’échanger des données, elles doivent donc connaitre leurs points d’entrée / sortie respectives. Pour ce faire elles utilisent notamment les types (donnés via les classes) qui permettent d’assurer que les différentes méthodes appelées dans une classe seront présentes et n’aboutiront pas à une erreur.

Les protocoles servent justement à ça, ils fournissent un ou plusieurs sous-types à chaque classes qui peuvent être interchangés. Vu que c’est pas forcément clair je vais vous montrer un exemple en code. Imaginons que l’on ait un tableau contenant une liste de UILabel comme ceci :

1
var tableau: [UILabel] = [UILabel(), UILabel()] // On initialise 2 UILabel vide pour l'exemple

et que l’on souhaite modifier le texte de tous les labels avec une seule fonction. Si vous avez bien compris les autres tutos du site cela devrait être une partie de plaisir à coder pour vous, je vais donc vous laisser créer cette fameuse fonction par vous-même ! Je vous donne quand même la correction mais si vous n’avez pas sur la créer, vous devriez relire certains chapitres.

Voir la correction
1
2
3
4
5
6
7
8
9
10
11
class Vue {

    var tableau: [UILabel] = [UILabel(), UILabel()]

    func setText(_ text: String) {
        for label in tableau {
            label.text = text
        }
    }

}

 

Facile. Compliquons un peu les choses (par ce que c’est ce que l’on aime faire) et disons que nous souhaitons également insérer dans notre tableau des champs de texte qui sont de type UITextField. Si l’on ajoute lesdits champs en effaçant le type de la variable tableau pour essayer de feinter Xcode, il va prendre par défaut le type commun de ces deux objets (à savoir UIView) et cela ne fait que décaler notre problème.

Sur la ligne label.text = text nous obtiendrons donc l’erreur Value of type ‘UIView’ has no member ‘text’ qui nous indique tout simplement que la variable text n’existe pas dans la classe UIView. Et comme UIView est déjà écrite par Apple, nous n’avons pas vraiment la possibilité de la modifier (la classe) afin d’y ajouter cet attribut. Alors comment faire ?

Je vois quelques doigts levés dans la classe, je suis fier de vous ! En effet c’est bien là que nous allons utiliser nos fameux protocoles.

De comment que ça marche ?

Créer un protocole va nous permettre de créer un type d’objet (comme avec une classe) dont on pourra faire hériter d’autres objets (comme une classe). La grande différence est qu’une classe ne peut hériter que d’une seule autre classe, alors qu’elle peut hériter d’une infinité de protocoles différents tant qu’ils n’entrent pas en conflit entre eux. Si nous continuons notre exemple, on pourrait imaginer que l’on ai créé deux classes distinctes pour relayer avec du code personnalisé les UITextField et UILabel de l’API d’Apple :

1
2
3
4
5
6
7
class Label: UILabel {
    // du code
}

class TextField: UITextField {
    // plus de code
}

Mais même comme ça on obtient deux types distincts que l’on ne peut donc pas définir dans un seul et même tableau. Nous allons donc les faire hériter d’un protocole commun et nous pourrons définir notre tableau sur le type du protocole, il pourra par conséquent accueillir toutes les classes héritant de ce dernier. Pour ce faire, nous allons déclarer notre protocole comme une classe (mais avec le mot-clé protocol), puis ajouter son type à la suite des autres types hérités dans nos classes à l’aide d’une simple virgule :

1
2
3
4
5
6
7
8
9
10
11
protocol TextView {
   
}

class Label: UILabel, TextView {
    // du code
}

class TextField: UITextField, TextView {
    // plus de code
}

De cette façon, nos deux classes ici ont un type commun dont nous allons pouvoir nous servir pour les regrouper dans un seul et même tableau :

1
var tableau : [TextView] = [Label(), TextField()] // Deux classes différentes regroupées par leur type commun

Mais alors vigilance : les variables incluses dans le tableau ne seront que du type TextView et il ne sera par conséquent pas possible (en tout cas de cette façon) d’accéder aux variables propres aux classes Label et TextField.

On va pouvoir inclure des variables et fonctions propres à notre protocole comme on le ferai dans une classe classique (lol) à la différence près qu’ils ne peuvent pas contenir de valeur, ou de code à exécuter. Eh oui, un protocole va simplement définir des points d’entrée-sortie mais le rôle d’attribution de valeur à une variable ou d’attribution de code à une fonction incombe toujours aux classes, les protocoles ne sont que des modèles.

Ajouter du code

Mais alors comment fonctionne le système de variable. Jusque là nous savons qu’une variable possède un nom, un type et une valeur ; dans un protocole une variable n’a pas de valeur définie, mais des accès réservés. On va pouvoir paramétrer chaque variable de notre protocol en lui indiquant si elle va pouvoir être lue, ou lue et écrite. Voyons comment déclarer tout ça :

1
2
3
protocol TextView {
    var texte: String { get set }
}

Une variable peut être en lecture / écriture (get set) ou en lecture seule (get) mais jamais en écriture seule (set)

Entre les accolades nous rajoutons les mots-clés get et set qui vont respectivement autoriser la variable à être lue et écrite. De cette façon, elle se comportera comme n’importe quelle variable de classe. Voyons d’ailleurs comment nous en servir dans une classe :

1
2
3
4
class Label: UILabel, TextView {
    var texte: String = "Mon texte"
   
}

Nous utilisons le nom de variable texte en français pour ne pas créer de conflit avec la variable text déjà existante. Eh oui, il faut faire attention ça…

Pour ajouter une fonction c’est encore plus simple, il suffit de déclarer son nom et ses points d’entrée / sortie (attributs et valeurs de retour) sans le code qu’elle contiendra (comme un header en C) :

1
2
3
4
5
protocol TextView {
    var texte: String { get set }

    func maFonction(attribut: String) -> Int
}

Ici, on crée une fonction maFonction qui acceptera une variable de type String en entrée et retournera une variable de type Int, à noter bien sûr que le type de retour n’est pas requis si la fonction ne retourne pas de variable. Encore une fois, le code exécuté n’est pas écrit ici, mais sera spécifique à chaque classe héritant du protocole. On fixe juste les contraintes de cette classe.

1
2
3
4
5
6
7
class Label: UILabel, TextView {
    var texte: String = "Mon texte"
   
    func maFonction(attribut: String) -> Int {
        return attribut.count // le nombre de caractères dans la chaîne
    }
}

Facile non ? Certains d’entre vous (ceux qui ont déjà testé) on remarqué qu’Xcode vous affichait un message d’erreur tant que la variable n’était pas présente dans la classe : Type ‘Label’ does not conform to protocol ‘TextView’ lorsque qu’il manque une seule variable ou fonction dans la classe qui hérite du protocole. Contrairement à l’héritage (unique) d’une autre classe, l’héritage multiple des protocoles nous oblige à déclarer chaque variable dans la classe en question. En effet, le protocole va nous assurer que la classe est conforme au modèle du protocole, mais n’initialisera aucune valeur.

De cette façon, nous pouvons avoir une même fonction agissant sur des objets de types différents sans aucun problème. Je vous laisse ré-écrire l’exemple du début de façon à ce que notre boucle itérant les valeurs de notre tableau de TextView puisse agir sur les deux types de champs de texte.

Voir la correction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protocol TextView {
    func setText(_ text: String)
}

class Label: UILabel, TextView {
    func setText(_ text: String) {
        self.text = text
    }
}

class TextField: UITextField, TextView {
    func setText(_ text: String) {
        self.text = text
    }
}

class Vue {
    var tableau : [TextView] = [Label(), TextField()]
    func setText(_ text: String) {
        for label in tableau {
            label.setText(text)
        }
    }
}

Oui le code utilisé dans les deux fonctions est le même ici car les deux super-classes comprennent une variable « text« , cependant il n’est pas possible d’y accéder depuis leur type commun qui ne le comprend pas : UIView.

Quid des lectures écritures ?

Que se passe-t-il si une variable est en lecture seule ? Cela signifie que la variable pourra être lue, mais jamais écrite en dehors de la classe héritante. Cela veut dire que dans notre exemple, la classe Label pourra écrire dans la variable texte (après tout, c’est sa variable), mais pas la classe Vue qui ne fait que manipuler nos objets TextView :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protocol TextView {
    var texte: String { get }
   
}

class Label: UILabel, TextView {
   
    // On définit une valeur par défaut
    var texte: String = "Mon texte"
   
    func fonction() {
      // On peut modifier la valeur depuis la classe héritante
      self.texte = "Un autre texte"
    }
}

class Vue {
    var tableau : [TextView] = [Label()]
    func setText(_ text: String) {
        for label in tableau {
            // Impossible de modifier la variable depuis une autre classe
            label.texte = "Nouveau texte"
           
            // Il est en revanche possible de lire la variable
            print(label.texte)
        }
    }
}

Sur la ligne label.texte, Xcode nous affichera l’erreur Cannot assign to property: ‘texte’ is a get-only property pour nous rappeller que nous n’avons pas accès à la modification de la variable.

 

Bon ! Ca me semble être pas mal pour cette fois non ? On en a pas tout à fait fini avec les classes, les types et les héritages mais le plus dur a été fait. On se retrouvera pour terminer ensemble les fondamentaux de la programmation orientée objet, en attendant je laisse votre cerveau refroidir et vous dit à la prochaine !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *