Il y a 2 années · 4 minutes · iOS, Mobile

Swift 2 et les Protocol Extensions

Swift 2 est disponible et avec lui, de nombreuses nouveautés dans le langage sont apparues. L’une des plus intéressantes est certainement Protocol Extension.

Derrière ce nom quelque peu barbare se cache en fait une fonctionnalité très simple : la possibilité d’étendre un protocole, de la même manière que l’on peut étendre une classe.

À quoi sert Protocol Extension ?

Vous avez certainement déjà connu ce genre de situation : vous implémentez un protocole sur plusieurs classes, puis vous vous rendez compte que la majorité du code est dupliquée.

protocol Bettable {
    var myBet: Int { get set }
    var min: Int { get set }

    func isBettable() -> Bool
}

struct MyStruct: Bettable {
    func isBettable() -> Bool {
        return self.myBet > 0 && self.myBet > self.min
    }
}

struct MyStruct2: Bettable {
    func isBettable() -> Bool {
        return self.myBet > 0 && self.myBet > self.min // duplication
    }
}

Vous vous retrouvez alors avec deux choix :

  1. Garder le code dupliqué
  2. Créer une classe abstraite

Si les deux propositions sont techniquement fonctionnelles, elles ont chacune leur lot d’inconvénients : maintenance difficile, risque de bug, incohérence de la hiérarchie…

Avec Swift 2 apparaît une alternative : les Protocol Extensions. Au lieu d’implémenter la définition de la méthode dans une classe, celle-ci est implementée directement au niveau du protocole !

Le fonctionnement est simple : comme pour une classe (en Swift 1 et en Objective-C), vous pouvez déclarer une ou plusieurs extensions sur un protocole afin de l’implémenter :

protocol Bettable {
    var myBet: Int { get set }
    var min: Int { get set }

    func isBettable() -> Bool
}

extension Bettable {
    func isBettable() -> Bool {
        return self.myBet > 0 && self.myBet > self.min
    }
}

// Pas de duplication !
struct MyStruct: Bettable {

}

// Pas de duplication !
struct MyStruct2: Bettable {

}

Vous évitez ainsi la duplication de code. Mieux encore, aucun héritage n’a été nécessaire : les classes se conformant au protocole ont automatiquement récupéré la définition !

Les propriétés des Protocol Extensions

Partialité

Un concept très intéressant d’une protocol extension est que vous pouvez l’implémenter partiellement : vous n’êtes pas obligés d’offrir une implémentation par défaut de toutes les méthodes.

protocol Awesome {
    func isAwesome() -> Bool
    func isNotAwesome() -> Bool
}

// nous ne savons pas comment implémenter isAwesome pour tous les cas
// par contre il est très vraisemblable que isNotAwesome soit juste l’opposé de isAwesome
extension Awesome {
    func isNotAwesome() -> Bool {
        return !self.isAwesome()
    }
}

Cela permet d’atteindre deux résultats intéressants :

  1. Avoir une alternative à l’annotation @optional des protocoles qui n’est pas disponible en Swift
  2. Utiliser l’extension protocol comme « abstract class » en implémentant le code commun dedans

Generics

Vous pouvez définir des contraintes pour appliquer vos extensions grâce aux Generics. Vous pouvez ainsi définir une même extension pour plusieurs cas de figure !

/**
Définit la méthode indexOf pour les classes respectant le protocole CollectionType
la méthode ne sera disponible que lorsque les éléments de la collection sont eux-même de type Equatable
*/
extension CollectionType where Generator.Element : Equatable {
    public func indexOf(element: Generator.Element) -> Index? {
        for i in self.indices {
            if self[i] == element { // contrainte Equatable
                return i
            }
        }

        return nil
    }
}


Vous pouvez également utiliser le mot clé Self afin de pouvoir adapter vos extensions en fonction de la classe qui adaptera le protocole.

protocol Loggable {
    func log()
}

extension Loggable where Self:String {
    func log() {
        print(self.description)
    }
}

Une condition à respecter cependant : le type utilisé dans la clause where se doit d’être une classe ou un protocole. Exit donc les enum ou les struct.

Conflits

Swift est assez intelligent pour vous indiquer lorsqu’il y a conflit entre deux extensions sur un type. Cela se matérialise par une erreur à la compilation, mais sous forme très générique : « Type 'C' does not conform to protocol 'A' ».

protocol A {
    func sayHello()
}

protocol B {
    func sayHello()
}

extension A {
    func sayHello() {
        print("hello #A")
    }
}

extension B {
    func sayHello() {
        print("hello #B")
    }
}

struct C : A, B { // le compilateur vous affichera "Type 'C' does not conform to protocol 'A'". Pareil pour 'B'.
}

En revanche, aucun souci lorsque vous faites de l’héritage de protocole, Swift comprend quelle méthode utiliser !

// Même code que l'exemple précédent, mais cette fois-ci
// B hérite de A
protocol B : A {
    func sayHello()
}

// Nous pourrions marquer C : B, mais ici nous montrons que A ne rentre pas en conflit avec B
struct C : A, B {
}

let c = C()
c.sayHello() //  affiche "hello #B"

Swift 2, une nouvelle manière de programmer

Swift 2 ouvre la porte vers une nouvelle manière de programmer au travers des protocol extension. Plus qu’une simple solution pour limiter la duplication de code, les protocol extension vont réellement vous simplifier la vie.

N’hésitez pas à regarder la vidéo Protocol-Oriented Programming in Swift de la WWDC 2015 pour en savoir plus dessus.

 

Jean-Christophe Pastant

Consultant iOS, fervent défenseur du code de qualité.

Laisser un commentaire

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