Programmation orientée protocole dans Swift 5.1

Les protocoles sont une propriété fondamentale de Swift. Ils jouent un rôle important dans les bibliothèques Swift standard et sont un moyen courant de coder l'abstraction. À bien des égards, ils sont similaires aux interfaces d'autres langages de programmation.

Dans ce didacticiel, nous vous présenterons une approche de développement d'applications appelée programmation orientée protocole, qui est devenue presque le cœur de Swift. C'est vraiment ce que vous devez comprendre lorsque vous apprenez Swift!

Dans ce guide, vous:

  • comprendre la différence entre la programmation orientée objet et la programmation orientée protocole;
  • comprendre les implémentations de protocole standard;
  • Découvrez comment étendre les fonctionnalités de la bibliothèque Swift standard;
  • Apprenez à étendre les protocoles avec des génériques.

Commencer


Imaginez que vous développez un jeu de course. Vos joueurs peuvent conduire des voitures, des motos et des avions. Et même voler sur des oiseaux, c'est un jeu, non? L'essentiel ici est qu'il existe un dofiga de toutes sortes de «choses» sur lesquelles vous pouvez conduire, voler, etc.

Une approche courante pour développer de telles applications est la programmation orientée objet. Dans ce cas, nous enfermons toute la logique dans certaines classes de base, dont nous héritons par la suite. Par conséquent, les classes de base doivent contenir la logique de «chef de file» et de «pilote».

Nous commençons le développement en créant des classes pour chaque véhicule. Nous reporterons les «Oiseaux» à plus tard, nous y reviendrons un peu plus tard.

Nous voyons que la voiture et la mototrès similaire dans la fonctionnalité, nous créons donc la classe de base MotorVehicle . La voiture et la moto seront héritées de MotorVehicle. De la même manière, nous créons la classe de base Aircraft , à partir de laquelle nous allons créer la classe Plane .

Vous pensez que tout va bien, mais - bam! - L'action de votre jeu se déroule au XXXe siècle, et certaines voitures peuvent déjà voler .

Donc, nous avions un bâillon. Swift n'a pas d'héritage multiple. Comment vos voitures volantes peuvent-elles être héritées de MotorVehicle et Aircraft? Créer une autre classe de base qui combine les deux fonctionnalités? Probablement pas, car il n'existe aucun moyen clair et simple d'y parvenir.

Et qu'est-ce qui sauvera notre jeu dans cette terrible situation? La programmation orientée protocole est pressée de vous aider!

À quoi ressemble la programmation orientée protocole?


Les protocoles vous permettent de regrouper des méthodes, fonctions et propriétés similaires pour les classes, les structures et les énumérations. Cependant, seules les classes vous permettent d'utiliser l'héritage de la classe de base.

L'avantage des protocoles dans Swift est qu'un objet peut se conformer à plusieurs protocoles.

Lorsque vous utilisez cette méthode, votre code devient plus modulaire. Considérez les protocoles comme des blocs de construction de fonctionnalités. Lorsque vous ajoutez une nouvelle fonctionnalité à un objet, le rendant conforme à un certain protocole, vous ne créez pas un objet complètement nouveau à partir de zéro, ce serait trop long. Au lieu de cela, vous ajoutez différents blocs de construction jusqu'à ce que l'objet soit prêt.

Passer d'une classe de base à des protocoles résoudra notre problème. Avec les protocoles, nous pouvons créer une classe FlyingCar qui correspond à la fois à MotorVehicle et à Aircraft. Bien, hein?

Faisons le code


Exécutez Xcode, créez une aire de jeux, enregistrez-la sous SwiftProtocols.playground, ajoutez ce code:

protocol Bird {
  var name: String { get }
  var canFly: Bool { get }
}

protocol Flyable {
  var airspeedVelocity: Double { get }
}


Compilez avec Command-Shift-Return pour vous assurer que tout est en ordre.

Ici, nous définissons un protocole Bird simple , avec le nom et les propriétés canFly . Ensuite, nous définissons le protocole Flyable avec la propriété airspeedVelocity .

Dans «l'ère pré-protocole», le développeur commencerait par la classe Flyable comme classe de base, puis, en utilisant l'héritage, définirait Bird et tout ce qui pourrait voler.

Mais dans la programmation orientée protocole, tout commence par un protocole. Cette technique nous permet d'encapsuler une esquisse d'une fonctionnelle sans classe de base.

Comme vous le verrez maintenant, cela rend le processus de définition de type beaucoup plus flexible.

Déterminer le type correspondant au protocole


Ajoutez ce code en bas de l'aire de jeux:

struct FlappyBird: Bird, Flyable {
  let name: String
  let flappyAmplitude: Double
  let flappyFrequency: Double
  let canFly = true

  var airspeedVelocity: Double {
    3 * flappyFrequency * flappyAmplitude
  }
}


Ce code définit une nouvelle structure FlappyBird qui est conforme à la fois au protocole Bird et au protocole Flyable. Sa propriété airspeedVelocity est un travail de Flappy Frequency et flappy Amplitude. La propriété canFly renvoie true.

Ajoutez maintenant les définitions de deux autres structures:

struct Penguin: Bird {
  let name: String
  let canFly = false
}

struct SwiftBird: Bird, Flyable {
  var name: String { "Swift \(version)" }
  let canFly = true
  let version: Double
  private var speedFactor = 1000.0
  
  init(version: Double) {
    self.version = version
  }

  // Swift is FASTER with each version!
  var airspeedVelocity: Double {
    version * speedFactor
  }
}


Le pingouin est un oiseau mais ne peut pas voler. C'est bien que nous n'utilisions pas d'héritage et n'ayons pas rendu tous les oiseaux volables !

Lorsque vous utilisez des protocoles, vous définissez les composants de la fonctionnalité et rendez tous les objets appropriés conformes au protocole.Nous

définissons ensuite SwiftBird , mais dans notre jeu, il existe plusieurs versions différentes. Plus le numéro de version est grand, plus son airspeedVelocity , qui est défini comme une propriété calculée, est élevé .

Cependant, il existe une certaine redondance. Chaque type d'oiseau doit définir une définition explicite de la propriété canFly, bien que nous ayons une définition du protocole Flyable. Il semble que nous ayons besoin d'un moyen de déterminer l'implémentation par défaut des méthodes de protocole. Eh bien, il existe des extensions de protocole pour cela.

Extension du protocole avec un comportement par défaut


Les extensions de protocole vous permettent de définir le comportement du protocole par défaut. Écrivez ce code immédiatement après la définition du protocole Bird:

extension Bird {
  // Flyable birds can fly!
  var canFly: Bool { self is Flyable }
}


Ce code définit l'extension du protocole Bird . Cette extension détermine que la propriété canFly renverra true lorsque le type sera conforme au protocole Flyable . En d'autres termes, aucun oiseau volant n'a plus besoin de définir explicitement canFly.

Supprimez maintenant let canFly = ... des définitions de FlappyBird, Penguin et SwiftBird. Compilez le code et assurez-vous que tout est en ordre.

Faisons les transferts


Les énumérations dans Swift peuvent être conformes aux protocoles. Ajoutez la définition d'énumération suivante:

enum UnladenSwallow: Bird, Flyable {
  case african
  case european
  case unknown
  
  var name: String {
    switch self {
    case .african:
      return "African"
    case .european:
      return "European"
    case .unknown:
      return "What do you mean? African or European?"
    }
  }
  
  var airspeedVelocity: Double {
    switch self {
    case .african:
      return 10.0
    case .european:
      return 9.9
    case .unknown:
      fatalError("You are thrown from the bridge of death!")
    }
  }
}


En définissant les propriétés appropriées, UnladenSwallow se conforme à deux protocoles - Bird et Flyable. De cette façon, la définition par défaut de canFly est implémentée.

Remplacement du comportement par défaut


Notre type UnladenSwallow , selon le protocole Bird , a automatiquement reçu une implémentation pour canFly . Cependant, nous avons besoin de UnladenSwallow.unknown pour renvoyer false pour canFly.

Ajoutez le code suivant ci-dessous:

extension UnladenSwallow {
  var canFly: Bool {
    self != .unknown
  }
}

Maintenant, seuls .african et .european retourneront true pour canFly. Vérifiez-le! Ajoutez le code suivant au bas de notre aire de jeux:

UnladenSwallow.unknown.canFly         // false
UnladenSwallow.african.canFly         // true
Penguin(name: "King Penguin").canFly  // false

Compilez l'aire de jeux et vérifiez les valeurs reçues avec celles indiquées dans les commentaires ci-dessus.

Ainsi, nous redéfinissons les propriétés et les méthodes de la même manière que l'utilisation de méthodes virtuelles dans la programmation orientée objet.

Expansion du protocole


Vous pouvez également rendre votre propre protocole conforme à un autre protocole de la bibliothèque standard Swift et définir le comportement par défaut. Remplacez la déclaration du protocole Bird par le code suivant:

protocol Bird: CustomStringConvertible {
  var name: String { get }
  var canFly: Bool { get }
}

extension CustomStringConvertible where Self: Bird {
  var description: String {
    canFly ? "I can fly" : "Guess I'll just sit here :["
  }
}

La conformité au protocole CustomStringConvertible signifie que votre type doit avoir une propriété de description. Au lieu d'ajouter cette propriété dans le type Bird et dans tous ses dérivés, nous définissons l'extension de protocole CustomStringConvertible, qui sera associée uniquement au type Bird.

Tapez UnladenSwallow.african au bas de l'aire de jeux. Compilez et vous verrez "Je peux voler".

Protocoles dans les bibliothèques standard Swift


Comme vous pouvez le voir, les protocoles sont un moyen efficace d'étendre et de personnaliser les types. Dans la bibliothèque standard Swift, cette propriété est également largement utilisée.

Ajoutez ce code au terrain de jeu:

let numbers = [10, 20, 30, 40, 50, 60]
let slice = numbers[1...3]
let reversedSlice = slice.reversed()

let answer = reversedSlice.map { $0 * 10 }
print(answer)

Vous savez probablement ce que ce code produira, mais vous pourriez être surpris des types utilisés ici.

Par exemple, slice n'est pas Array, mais ArraySlice. Il s'agit d'un «wrapper» spécial qui fournit un moyen efficace de travailler avec des parties de la matrice. Par conséquent, reverseSlice est un ReversedCollection <ArraySlice>.

Heureusement, la fonction de carte est définie comme une extension du protocole Sequence, qui correspond à tous les types de collection. Cela nous permet d'appliquer la fonction de carte à la fois à Array et à ReversedCollection sans remarquer la différence. Bientôt, vous profiterez de cette astuce utile.

À vos marques


Jusqu'à présent, nous avons identifié plusieurs types conformes au protocole Bird. Maintenant, nous allons ajouter quelque chose de complètement différent:

class Motorcycle {
  init(name: String) {
    self.name = name
    speed = 200.0
  }

  var name: String
  var speed: Double
}

Ce type n'a rien à voir avec les oiseaux et les vols. Nous voulons organiser une course de moto avec des pingouins. Il est temps de faire démarrer cette étrange entreprise.

Mettre tous ensemble


Afin d'unir en quelque sorte ces différents coureurs, nous avons besoin d'un protocole commun pour la course. Nous pouvons faire tout cela sans même toucher à tous les types que nous avons créés auparavant, à l'aide d'une merveilleuse chose appelée modélisation rétroactive. Ajoutez simplement ceci au terrain de jeu:

// 1
protocol Racer {
  var speed: Double { get }  // speed is the only thing racers care about
}

// 2
extension FlappyBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension SwiftBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension Penguin: Racer {
  var speed: Double {
    42  // full waddle speed
  }
}

extension UnladenSwallow: Racer {
  var speed: Double {
    canFly ? airspeedVelocity : 0.0
  }
}

extension Motorcycle: Racer {}

// 3
let racers: [Racer] =
  [UnladenSwallow.african,
   UnladenSwallow.european,
   UnladenSwallow.unknown,
   Penguin(name: "King Penguin"),
   SwiftBird(version: 5.1),
   FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
   Motorcycle(name: "Giacomo")]

Voici ce que nous faisons ici: tout d'abord, définissez le protocole Racer. C'est tout ce qui peut participer aux courses. Ensuite, nous convertissons tous nos types précédemment créés dans le protocole Racer. Et enfin, nous créons un tableau qui contient des instances de chacun de nos types.

Compilez le terrain de jeu pour que tout soit en ordre.

Vitesse de pointe


Nous écrivons une fonction pour déterminer la vitesse maximale des coureurs. Ajoutez ce code à la fin de l'aire de jeux:

func topSpeed(of racers: [Racer]) -> Double {
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

topSpeed(of: racers) // 5100

Ici, nous utilisons la fonction max pour trouver le pilote à la vitesse maximale et le renvoyer. Si le tableau est vide, alors 0,0 est renvoyé.

Rendre la fonction plus générale


Supposons que le tableau Racers soit suffisamment grand et que nous devons trouver la vitesse maximale non pas dans l'ensemble du tableau, mais dans une partie de celui-ci. La solution est de changer topSpeed ​​(de :) pour qu'il prenne comme argument non pas spécifiquement un tableau, mais tout ce qui est conforme au protocole Sequence.

Remplacez notre implémentation de topSpeed ​​(de :) comme suit:

// 1
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double
    /*2*/ where RacersType.Iterator.Element == Racer {
  // 3
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

  1. RacersType est le type d'argument générique de notre fonction. Il peut s'agir de tout ce qui est conforme au protocole Sequence.
  2. où détermine que le contenu de la séquence doit être conforme au protocole Racer.
  3. Le corps de la fonction elle-même reste inchangé.

Vérifiez en ajoutant ceci à la fin de notre aire de jeux:

topSpeed(of: racers[1...3]) // 42

Maintenant, notre fonction fonctionne avec tout type répondant au protocole Sequence, y compris ArraySlice.

Rendre la fonction plus «rapide»


Secret: vous pouvez faire encore mieux. Ajoutez ceci tout en bas:

extension Sequence where Iterator.Element == Racer {
  func topSpeed() -> Double {
    self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
  }
}

racers.topSpeed()        // 5100
racers[1...3].topSpeed() // 42

Et maintenant, nous avons étendu le protocole Sequence lui-même avec topSpeed ​​(). Elle n'est applicable que lorsque Sequence contient le type Racer.

Comparateurs de protocoles


Une autre caractéristique des protocoles Swift est la façon dont vous définissez les opérateurs d'égalité des objets ou leur comparaison. Nous écrivons ce qui suit:

protocol Score {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
}

Avec le protocole Score, vous pouvez écrire du code qui traite tous les éléments de ce type d'une manière. Mais si vous obtenez un type très spécifique, tel que RacingScore, vous ne le confondrez pas avec d'autres dérivés du protocole Score.

Nous voulons que les scores soient comparés pour voir qui a le score le plus élevé. Avant Swift 3, les développeurs devaient écrire des fonctions globales pour définir un opérateur pour un protocole. Nous pouvons maintenant définir ces méthodes statiques dans le modèle lui-même. Pour ce faire, nous remplaçons les définitions de Score et RacingScore comme suit:

protocol Score: Comparable {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
  
  static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
    lhs.value < rhs.value
  }
}

Nous avons toute la logique de RacingScore en un seul endroit. Le protocole comparable vous oblige à définir une implémentation uniquement pour la fonction inférieure à. Toutes les autres fonctions de comparaison seront implémentées automatiquement, sur la base de l'implémentation de la fonction "moins de" que nous avons créée.

Essai:

RacingScore(value: 150) >= RacingScore(value: 130) // true

Apporter des modifications à l'objet


Jusqu'à présent, chaque exemple a montré comment ajouter des fonctionnalités. Mais que se passe-t-il si nous voulons créer un protocole qui change quelque chose dans un objet? Cela peut être fait en utilisant des méthodes de mutation dans notre protocole.

Ajoutez un nouveau protocole:

protocol Cheat {
  mutating func boost(_ power: Double)
}

Ici, nous définissons un protocole qui nous permet de tricher. Comment? Modification arbitraire du contenu du boost.

Créez maintenant une extension SwiftBird conforme au protocole Cheat:

extension SwiftBird: Cheat {
  mutating func boost(_ power: Double) {
    speedFactor += power
  }
}

Ici, nous implémentons la fonction boost (_ :), augmentant le speedFactor de la valeur transmise. Le mot-clé mutant fait comprendre à la structure qu'une de ses valeurs sera modifiée par cette fonction.

Vérifiez-le!
var swiftBird = SwiftBird(version: 5.0)
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030

Conclusion


Ici, vous pouvez télécharger le code source complet de l'aire de jeux.

Vous avez découvert les possibilités de la programmation orientée protocole en créant des protocoles simples et en augmentant leurs capacités à l'aide d'extensions. En utilisant l'implémentation par défaut, vous donnez aux protocoles le «comportement» approprié. Presque comme avec les classes de base, mais en mieux, car tout cela s'applique également aux structures et aux énumérations.

Vous avez également vu que l'extension de protocole s'applique aux protocoles Swift sous-jacents.

Vous trouverez ici le guide officiel du protocole .

Vous pouvez également regarder une excellente conférence de la WWDC sur la programmation orientée protocole.

Comme avec tout paradigme de programmation, il existe un danger de s'emballer et de commencer à utiliser les protocoles à gauche et à droite. Voici une note intéressante sur les dangers des décisions de style silver bullet.

Source: https://habr.com/ru/post/undefined/


All Articles