Programación Orientada a Protocolo en Swift 5.1

Los protocolos son una propiedad fundamental de Swift. Desempeñan un papel importante en las bibliotecas estándar de Swift y son una forma común de codificar la abstracción. En muchos sentidos, son similares a las interfaces en otros lenguajes de programación.

En este tutorial, le presentaremos un enfoque de desarrollo de aplicaciones llamado programación orientada al protocolo, que se ha convertido casi en el núcleo de Swift. ¡Esto es realmente lo que necesitas entender al aprender Swift!

En esta guía, usted:

  • entender la diferencia entre la programación orientada a objetos y la programación orientada a protocolos;
  • entender las implementaciones de protocolo estándar;
  • Aprenda a ampliar la funcionalidad de la biblioteca Swift estándar;
  • Aprenda a extender protocolos con genéricos.

Empezando


Imagina que estás desarrollando un juego de carreras. Sus jugadores pueden conducir automóviles, motocicletas y aviones. E incluso volar sobre pájaros, este es un juego, ¿verdad? Lo principal aquí es que hay una dofiga de todo tipo de "cosas" en las que puedes conducir, volar, etc.

Un enfoque común para desarrollar tales aplicaciones es la programación orientada a objetos. En este caso, incluimos toda la lógica en algunas clases base, de las cuales heredamos de allí en adelante. Las clases base, por lo tanto, deben contener la lógica de "líder" y "piloto".

Comenzamos el desarrollo creando clases para cada vehículo. Pospondremos a los "Pájaros" hasta más tarde, volveremos a ellos un poco más tarde.

Vemos que coche y motomuy similar en funcionalidad, por lo que creamos la clase base MotorVehicle . El automóvil y la motocicleta se heredarán de MotorVehicle. Del mismo modo, creamos la clase base de Aeronave , a partir de la cual crearemos la clase Plane .

Crees que todo está bien, pero ¡bam! - La acción de tu juego tiene lugar en el siglo XXX, y algunos autos ya pueden volar .

Entonces, tuvimos una mordaza. Swift no tiene herencia múltiple. ¿Cómo se pueden heredar sus autos voladores de MotorVehicle y Aircraft? ¿Crear otra clase base que combine ambas funcionalidades? Lo más probable es que no, ya que no existe una forma clara y sencilla de lograrlo.

¿Y qué salvará nuestro juego en esta terrible situación? ¡La programación orientada al protocolo tiene prisa por ayudar!

¿Cómo es la programación orientada al protocolo?


Los protocolos le permiten agrupar métodos, funciones y propiedades similares para clases, estructuras y enumeraciones. Sin embargo, solo las clases le permiten usar la herencia de la clase base.

La ventaja de los protocolos en Swift es que un objeto puede ajustarse a múltiples protocolos.

Al usar este método, su código se vuelve más modular. Piense en los protocolos como bloques de construcción de funcionalidad. Cuando agrega una nueva funcionalidad a un objeto, haciéndolo cumplir con un determinado protocolo, no está creando un objeto completamente nuevo desde cero, sería demasiado largo. En su lugar, agrega diferentes bloques de construcción hasta que el objeto esté listo.

Pasar de una clase base a protocolos resolverá nuestro problema. Con protocolos, podemos crear una clase FlyingCar que coincida tanto con MotorVehicle como con Aircraft. Buena, ¿eh?

Hagamos el codigo


Ejecute Xcode, cree un parque infantil, guárdelo como SwiftProtocols.playground, agregue este código:

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

protocol Flyable {
  var airspeedVelocity: Double { get }
}


Compile con Command-Shift-Return para asegurarse de que todo esté en orden.

Aquí definimos un protocolo Bird simple , con el nombre y las propiedades de canFly . Luego definimos el protocolo Flyable con la propiedad airspeedVelocity .

En la "era previa al protocolo", el desarrollador comenzaría con la clase Flyable como la clase base, y luego, utilizando la herencia, definiría Bird y todo lo demás que podría volar.

Pero en la programación orientada al protocolo, todo comienza con un protocolo. Esta técnica nos permite encapsular un boceto de un funcional sin una clase base.

Como verá ahora, esto hace que el proceso de diseño de tipo sea mucho más flexible.

Determinar el tipo correspondiente al protocolo.


Agregue este código en la parte inferior del patio de recreo:

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

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


Este código define una nueva estructura FlappyBird que se ajusta tanto al protocolo Bird como al protocolo Flyable. Su propiedad AirspeedVelocity es un trabajo de Flappy Frequency y Flappy Amplitude. La propiedad canFly devuelve verdadero.

Ahora agregue las definiciones de dos estructuras más:

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
  }
}


Penguin es un pájaro pero no puede volar. ¡Es bueno que no usemos la herencia y no hayamos hecho volar a todas las aves !

Al usar protocolos, usted define los componentes de lo funcional y hace que todos los objetos adecuados se ajusten al protocolo.

Luego definimos SwiftBird , pero en nuestro juego hay varias versiones diferentes. Cuanto mayor sea el número de versión, mayor será su velocidad de velocidad aérea , que se define como una propiedad calculada.

Sin embargo, hay algo de redundancia. Cada tipo de Bird debe definir una definición explícita de la propiedad canFly, aunque tenemos una definición del protocolo Flyable. Parece que necesitamos una forma de determinar la implementación predeterminada de los métodos de protocolo. Bueno, hay extensiones de protocolo para esto.

Extendiendo el protocolo con comportamiento predeterminado


Las extensiones de protocolo le permiten establecer el comportamiento predeterminado del protocolo. Escriba este código inmediatamente después de la definición del protocolo Bird:

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


Este código define la extensión del protocolo Bird . Esta extensión determina que la propiedad canFly volverá verdadera cuando el tipo se ajuste al protocolo Flyable . En otras palabras, cualquier pájaro Flyable ya no necesita establecer explícitamente canFly.

Ahora elimine let canFly = ... de las definiciones de FlappyBird, Penguin y SwiftBird. Compile el código y asegúrese de que todo esté en orden.

Hagamos las transferencias


Las enumeraciones en Swift pueden ajustarse a los protocolos. Agregue la siguiente definición de enumeración:

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!")
    }
  }
}


Al definir las propiedades apropiadas, UnladenSwallow se ajusta a dos protocolos: Bird y Flyable. De esta manera, se implementa la definición predeterminada para canFly.

Anular el comportamiento predeterminado


Nuestro tipo UnladenSwallow , de acuerdo con el protocolo Bird , recibió automáticamente una implementación para canFly . Sin embargo, necesitamos UnladenSwallow.unknown para devolver false para canFly.

Agregue el siguiente código a continuación:

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

Ahora solo .african y .european serán verdaderos para canFly. ¡Echale un vistazo! Agregue el siguiente código en la parte inferior de nuestro patio de recreo:

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

Compile el patio de juegos y verifique los valores recibidos con los indicados en los comentarios anteriores.

Por lo tanto, redefinimos propiedades y métodos de la misma manera que usando métodos virtuales en la programación orientada a objetos.

Protocolo de expansión


También puede hacer que su propio protocolo se ajuste a otro protocolo de la biblioteca estándar de Swift y definir el comportamiento predeterminado. Reemplace la declaración del protocolo Bird con el siguiente código:

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 :["
  }
}

El cumplimiento del protocolo CustomStringConvertible significa que su tipo debe tener una propiedad de descripción. En lugar de agregar esta propiedad en el tipo Bird y en todos sus derivados, definimos la extensión de protocolo CustomStringConvertible, que se asociará solo con el tipo Bird.

Escriba UnladenSwallow.african en la parte inferior del patio de recreo. Compile y verá "Puedo volar".

Protocolos en bibliotecas estándar Swift


Como puede ver, los protocolos son una forma efectiva de extender y personalizar tipos. En la biblioteca estándar de Swift, esta propiedad también se usa ampliamente.

Agregue este código al patio de recreo:

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)

Probablemente sepa qué generará este código, pero puede sorprenderse con los tipos utilizados aquí.

Por ejemplo, slice no es Array, sino ArraySlice. Este es un "contenedor" especial que proporciona una forma eficiente de trabajar con partes de la matriz. En consecuencia, reverseedSlice es una ReversedCollection <ArraySlice>.

Afortunadamente, la función de mapa se define como una extensión del protocolo de secuencia, que corresponde a todos los tipos de colección. Esto nos permite aplicar la función de mapa a Array y ReversedCollection y no notar la diferencia. Pronto aprovecharás este útil truco.

En sus marcas


Hasta ahora, hemos identificado varios tipos que cumplen con el protocolo Bird. Ahora agregaremos algo completamente diferente:

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

  var name: String
  var speed: Double
}

Este tipo no tiene nada que ver con pájaros y vuelos. Queremos organizar una carrera de motos con pingüinos. Es hora de traer esta extraña compañía al comienzo.

Poniendolo todo junto


Para unir de alguna manera a corredores tan diferentes, necesitamos un protocolo común para competir. Podemos hacer todo esto sin siquiera tocar todos los tipos que creamos antes, con la ayuda de algo maravilloso llamado modelado retroactivo. Solo agregue esto al patio de recreo:

// 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")]

Esto es lo que hacemos aquí: primero, defina el protocolo Racer. Esto es todo lo que puede participar en las carreras. Luego lanzamos todos nuestros tipos creados previamente al protocolo Racer. Y finalmente, creamos una matriz que contiene instancias de cada uno de nuestro tipo.

Compile el patio de juegos para que todo esté en orden.

Velocidad máxima


Escribimos una función para determinar la velocidad máxima de los corredores. Agregue este código al final del patio de recreo:

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

topSpeed(of: racers) // 5100

Aquí usamos la función max para encontrar al piloto a la velocidad máxima y devolverlo. Si la matriz está vacía, se devuelve 0.0.

Hacer la función más general


Supongamos que la matriz Racers es lo suficientemente grande y necesitamos encontrar la velocidad máxima no en toda la matriz, sino en alguna parte de ella. La solución es cambiar topSpeed ​​(of :) para que tome como argumento no específicamente una matriz, sino todo lo que se ajuste al protocolo de secuencia.

Reemplace nuestra implementación de topSpeed ​​(de :) de la siguiente manera:

// 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 es el tipo de argumento genérico de nuestra función. Puede ser cualquier cosa que se ajuste al protocolo de secuencia.
  2. donde determina que el contenido de la secuencia debe cumplir con el protocolo Racer.
  3. El cuerpo de la función en sí permanece sin cambios.

Verifique agregando esto al final de nuestro patio de recreo:

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

Ahora nuestra función funciona con cualquier tipo que cumpla con el protocolo de secuencia, incluido ArraySlice.

Hacer la función más "rápida"


Secreto: puedes hacerlo aún mejor. Agregue esto en la parte inferior:

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

Y ahora hemos expandido el protocolo de secuencia con topSpeed ​​(). Solo es aplicable cuando Sequence contiene el tipo Racer.

Comparadores de protocolos


Otra característica de los protocolos Swift es cómo define los operadores de igualdad de los objetos o su comparación. Escribimos lo siguiente:

protocol Score {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
}

Con el protocolo Score, puede escribir código que trate todos los elementos de este tipo de una manera. Pero si obtiene un tipo muy específico, como RacingScore, no lo confundirá con otros derivados del protocolo Score.

Queremos comparar los puntajes para ver quién tiene el puntaje más alto. Antes de Swift 3, los desarrolladores necesitaban escribir funciones globales para definir un operador para un protocolo. Ahora podemos definir estos métodos estáticos en el modelo mismo. Hacemos esto reemplazando las definiciones de Score y RacingScore de la siguiente manera:

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

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

Tenemos toda la lógica para RacingScore en un solo lugar. El protocolo comparable requiere que defina una implementación solo para la función menor que. Todas las demás funciones de comparación se implementarán automáticamente, en función de la implementación de la función "menor que" que creamos.

Pruebas:

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

Hacer cambios al objeto


Hasta ahora, cada ejemplo ha demostrado cómo agregar funcionalidad. Pero, ¿qué pasa si queremos hacer un protocolo que cambie algo en un objeto? Esto se puede hacer usando métodos de mutación en nuestro protocolo.

Agregar un nuevo protocolo:

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

Aquí definimos un protocolo que nos permite hacer trampa. ¿Cómo? Cambiando arbitrariamente el contenido de boost.

Ahora cree una extensión SwiftBird que se ajuste al protocolo Cheat:

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

Aquí implementamos la función boost (_ :), aumentando el speedFactor por el valor transmitido. La palabra clave mutante hace que la estructura comprenda que uno de sus valores será cambiado por esta función.

¡Echale un vistazo!
var swiftBird = SwiftBird(version: 5.0)
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030

Conclusión


Aquí puede descargar el código fuente completo para el patio de recreo.

Aprendió sobre las posibilidades de la programación orientada a protocolos creando protocolos simples y aumentando sus capacidades con la ayuda de extensiones. Usando la implementación predeterminada, le da a los protocolos el "comportamiento" apropiado. Casi como con las clases base, pero solo mejor, ya que todo esto también se aplica a estructuras y enumeraciones.

También vio que la extensión del protocolo se aplica a los protocolos Swift subyacentes.

Aquí encontrará la guía de protocolo oficial .

También puede ver una excelente conferencia de WWDC sobre programación orientada a protocolos.

Como con cualquier paradigma de programación, existe el peligro de dejarse llevar y comenzar a usar los protocolos de izquierda y derecha. Aquí hay una nota interesante sobre los peligros de las decisiones de estilo bala de plata.

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


All Articles