Swift – Protocolos

El día de hoy aprenderemos sobre los Protocolos, exploraremos que son y como nos ayudan. Descubriremos como estos pueden transformar la manera en la que escribimos código.

¿Qué es un protocolo?

Un protocolo normaliza cierto comportamiento, se encuentra conformado por métodos, propiedades y otros requisitos que se adaptan a un modelo en particular.

Luego de establecer el «protocolo a seguir» ya podemos definir una implementación real a las propiedades y métodos que conforman el modelo. Esto lo podemos hacer en clases, estructuras o enumeraciones.

¡Pero vamos por partes y poco a poco, comencemos!

Sintaxis

La sintaxis de un protocolo es bien simple:

protocol SomeProtocol {

   // La definición del protocolo va aquí

} // SomeProtocol

Nuestros tipos pueden adoptar un protocolo en particular mediante la colocación del nombre del protocolo después del nombre del tipo, separados por dos puntos, como parte de su definición. También podemos adoptar múltiples protocolos sencillamente separándolos por comas:

struct SomeStructure: FirstProtocol, AnotherProtocol {

   // La definición de la estructura va aquí

} // SomeStructure

en este ejemplo podemos ver una estructura de nombre SomeStructure que adopta dos protocolos, FirstProtocol y AnotherProtocol.

En el caso de las clases y específicamente de aquellas que heredan de una clase base tendríamos que especificar primero la clase base y luego los protocolos, de la siguiente forma:

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {

   // La definición de la clase va aquí

} // SomeClass

Propiedades

Un protocolo puede requerir a cualquier tipo conforme al mismo, proporcionar una propiedad de instancia o de tipo con un nombre y un tipo particular. El protocolo no especifica si la propiedad debe ser una propiedad almacenada o una propiedad computada, simplemente se especifica el nombre de la propiedad requerida y el tipo.

El protocolo también especifica si cada propiedad debe incorporar un bloque get o get y set.

Los requisitos de una propiedad siempre se declaran como propiedades variables, con la palabra clave var como prefijo. Las propiedades que implementen bloques get y set son aquellas donde indicamos { get set } después de su declaración de tipo, lógicamente aquellas que solo necesiten de un bloque get solamente escribirán { get }. Aquí un ejemplo:

protocol SomeProtocol {

   var mustBeSettable: Int { get set }

   var doesNotNeedToBeSettable: Int { get }

} // SomeProtocol

Métodos

En el caso de los métodos, los protocolos pueden requerir métodos de instancia y de tipo que sean implementados por los tipos que adoptan los protocolos.

Estos métodos se incluyen como parte de la definición de los protocolos exactamente de la misma manera que con los métodos de instancia y de tipo, pero sin llaves, es decir sin un cuerpo, se permiten los parámetros variadic y están sujetos a las mismas reglas que para los métodos normales. Los valores por defecto, sin embargo, no pueden ser especificados dentro de una definición de protocolo. Veamos un ejemplo:

protocol RandomNumberGenerator {

   func random() -> Double

} // RandomNumberGenerator

en este ejemplo el protocolo RandomNumberGenerator obliga a todo aquel que lo adopte a definir una función / método de nombre random y que devuelva un valor Double.

Como hemos podido constatar, los protocolos no asumen como los métodos ni las propiedades computadas son implementadas, esto queda del lado de las clases, estructuras o enumeraciones que lo adopten.

Aunque evidentemente cuando creamos un protocolo detrás del sentido de su existencia hay un objetivo, en el caso anterior sería generar un número aleatoriamente y manejamos solamente ciertas necesidades básicas que las propiedades y métodos tienen que implementar.

Dígase parámetros, tipos de retorno, si el método muta algún parámetro interno pues lo marcamos con la palabra clave mutating y así, pero el algoritmo de generación de números aleatorios tendrá que ser implementado por los clientes del protocolo.

Inicializadores

Los protocolos pueden requerir inicializadores específicos para todos aquellos que lo adopten. Estos son declarados como parte de la definición del protocolo y de la misma manera como lo hacemos siempre pero sin el cuerpo del mismo:

protocol SomeProtocol {

   init(someParameter: Int)

} // SomeProtocol

en el caso de las clases cuando hacemos esto, el compilador nos obliga a que marquemos el inicializador con el modificador required. Este modificador nos va a obligar a que todas las clases que hereden de esta implementen el mismo inicializador, aquí un ejemplo:

class SomeClass: SomeProtocol {

   required init(someParameter: Int) {

      // Aquí estaría la inicialización de la clase

   } // init

} // SomeClass

Protocolos como tipos

Los protocolos en realidad no implementan ninguna funcionalidad en sí mismos. No obstante, cualquier protocolo que es creado se convertirá una vez adoptado en un tipo de dato con todas las de la ley, tal y como ocurre cuando heredamos de una clase que por esto la clase base no deja de ser un tipo válido para el sistema.

Debido a que es un tipo, podemos utilizar un protocolo en muchos lugares donde se permiten otros tipos, incluyendo:

  • Como un tipo de parámetro o tipo de retorno de una función, método o inicializador.
  • Como el tipo de una constante, variable o propiedad.
  • Como el tipo de los elementos en un arreglo, diccionario, u otro contenedor.

Analicemos un ejemplo de un protocolo siendo usado como un tipo:

protocol RandomNumberGenerator {

   func random() -> Double

} // RandomNumberGenerator

class LinearCongruentialGenerator: RandomNumberGenerator {

   var lastRandom = 42.0
   let m = 139968.0
   let a = 3877.0
   let c = 29573.0

   func random() -> Double {

      lastRandom = ((lastRandom * a + c) % m)

      return lastRandom / m

   } // random

} // LinearCongruentialGenerator

class Dice {

   let sides: Int
   let generator: RandomNumberGenerator

   init(sides: Int, generator: RandomNumberGenerator) {

      self.sides = sides
      self.generator = generator

   } // init

   func roll() -> Int {

      return Int(generator.random() * Double(sides)) + 1

   } // roll

} // Dice

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())

for _ in 1...5 {

   print("Random dice roll is \(d6.roll())")

} // for

En las primeras líneas definimos el protocolo llamado RandomNumberGenerator con un solo método llamado random y luego tenemos la declaración de la clase LinearCongruentialGenerator que adopta el protocolo e implementa el método random con un algoritmo que genera un número pseudo-aleatorio.

De las líneas 24 a la 42 definimos la clase Dice (dado), la cual en la línea 27 declara una propiedad constante de tipo RandomNumberGenerator y a esto es a lo que nos referíamos, estamos declarando una propiedad cuyo tipo es un protocolo, esto nos enmascara el valor final de esta propiedad, ya que solamente sabemos que será una estructura o clase que adopte el protocolo RandomNumberGenerator pero no sabemos que algoritmo implementará, esta elección queda del lado del cliente.

La función roll de la clase Dice viene a simular que el dado ha sido lanzado y se apoya en nuestra propiedad generator para complementar el resultado, en este caso la cara del dado.

En la línea 44 creamos un dado llamado d6 y lo inicializamos con un dado de 6 caras y con un algoritmo de generación de números pseudo-aleatorios llamado LinearCongruentialGenerator, pero quizás otro cliente prefiera otro algoritmo pues si este adopta el protocolo RandomNumberGenerator también podrá ser pasado como argumento al dado sin tener que modificar nada en su código.

Finalizamos en las líneas 46 a la 50 donde un bucle for simula 5 lanzamientos del dado y donde nos valemos de la función roll de nuestro dado para simular los resultados correspondientes.

La salida en pantalla es la siguiente:

Random dice roll is 3
Random dice roll is 5
Random dice roll is 4
Random dice roll is 5
Random dice roll is 4

Herencia

Un protocolo puede heredar de uno o más protocolos y puede añadir requisitos adicionales a aquellos heredados. La sintaxis de la herencia protocolo es similar a la sintaxis de la herencia de clases, pero con la posibilidad de heredar de múltiples protocolos:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {

   // La definición va aquí

} // InheritingProtocol

He aquí un ejemplo de un protocolo que hereda de TextRepresentable:

protocol PrettyTextRepresentable: TextRepresentable {

   var prettyTextualDescription: String { get }

} // PrettyTextRepresentable

El protocolo PrettyTextRepresentable hereda los requerimientos de TextRepresentable y añade a prettyTextualDescription, por lo que todo aquel que adopte a PrettyTextRepresentable tendrá que implementar todos los requerimientos que este herede y defina.

Protocolos class-only

Como parte de la flexibilidad que tienen los protocolos también los podemos limitar a que solamente puedan ser adoptados por clases, es decir que ni las estructuras ni las enumeraciones podrían adoptar estos protocolos.

Esto lo logramos mediante la adición de la palabra clave class a la lista de herencia de un protocolo, esta palabra clave siempre debe aparecer en primer lugar en la lista:

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {

   // La definición va aquí

} // SomeClassOnlyProtocol

el protocolo SomeClassOnlyProtocol solamente puede ser adoptado por clases, este a su vez hereda del protocolo SomeInheritedProtocol y en caso que una clase adopte su definición esto generará un error en tiempo de compilación.

Composición

En ciertas ocaciones puede ser útil que mediante un tipo podamos hacer referencia a múltiples protocolos de una vez, esto lo podemos lograr a través de una composición de protocolos.

Estas composiciones tienen la forma protocol<SomeProtocol, AnotherProtocol> y podemos listar tantos protocolos como queramos (seguramente debe de haber un límite pero no he podido averiguarlo) siempre dentro de los corchetes angulares (<>) y separados por comas.

Aquí podemos ver un ejemplo donde combinamos dos protocolos (llamados Names y Aged) en una sola composición, la cual es requerida como parámetro en una función:

protocol Named {

   var name: String { get }

} // Named

protocol Aged {

   var age: Int { get }

} // Aged

struct Person: Named, Aged {

   var name: String
   var age: Int

} // Person

func wishHappyBirthday(celebrator: protocol<Named, Aged>) {

   print("Happy birthday \(celebrator.name) - you're \(celebrator.age)!")

} // wishHappyBirthday

let birthdayPerson = Person(name: "Nery", age: 15)

wishHappyBirthday(birthdayPerson)

de la línea 13 a la 18 hemos creado la estructura Person que adopta a los dos protocolos que hemos definido más arriba, en la líneas 20 a la 24 tenemos a la función wishHappyBirthday la cual recibe como argumento una composición de protocolos o lo que sería lo mismo que decir, una clase, estructura o enumeración que adopte ambos protocolos.

Comprobación

En casos como aquel donde tenemos un arreglo de tipos AnyObject y queremos iterar a través de él y verificar cuales de sus elementos adoptan cierto protocolo, en estos casos, podemos hacer lo siguiente:

protocol HasArea {

   var area: Double { get }

} // HasArea

class Circle: HasArea {

   let pi = 3.1415927
   var radius: Double

   var area: Double {

      return pi * radius * radius

   } // area

   init(radius: Double) {

      self.radius = radius

   } // init

} // Circle

class Country: HasArea {

   var area: Double

   init(area: Double) {

      self.area = area

   } // init

} // Country

class Animal {

   var legs: Int

   init(legs: Int) {

      self.legs = legs

   } // init

} // Animal

let objects: [AnyObject] = [Circle(radius: 2.0), Country(area: 243_610), Animal(legs: 4)]

for object in objects {

   if let objectWithArea = object as? HasArea {

      print("Area is \(objectWithArea.area)")

   } else {

      print("Something that doesn't have an area")

   } // else

} // for

al inicio creamos el protocolo HasArea seguido de tres clases, las dos primeras adoptan este protocolo y la segunda no, ya que el área de un animal usualmente no es una dato importante, en la línea 50 creamos un arreglo llamado objects y lo inicializamos con una de instancia de cada una de las clases anteriores.

En las líneas 52 a la 64 es donde se encuentra la parte interesante de este ejemplo, en estas iteramos por el arreglo, pero específicamente en la 54 es donde comprobamos si el elemento de ese ciclo del bucle adopta el protocolo HasArea o no, en este caso haciendo uso del operador downcasting (as?).

En Swift podemos hacer uso de los operadores check (is) y downcasting (as? / as!) para comprobar si un objeto ya sea una clase, estructura o enumeración adoptan cierto protocolo o también para hacer un casting a un protocolo en especifico. Como hemos visto en el ejemplo anterior el chequeo y el casting siguen exactamente la misma sintaxis que cuando hacemos lo mismo con un tipo de dato cualquiera:

  • El operador is retorna true si la instancia adopta el protocolo y false en caso contrario.
  • La versión as? del operador downcast retorna un valor opcional del tipo de protocolo, este valor será nil en caso de que la instancia no adopte el protocolo en cuestión.
  • En el caso de la versión as! del operador downcast se fuerza el downcast (casting hacia el subtipo) hacia el tipo de protocolo, en caso de que el downcast no se ejecute satisfactoriamente se genera un error en tiempo de ejecución.

La salida en pantalla del código anterior sería:

Area is 12.5663708
Area is 243610.0
Something that doesn't have an area

Implementaciones por defecto

Las extensiones de protocolos también las podemos usar para declarar implementaciones por defecto de métodos o propiedades, aunque si alguno de los tipos que implementan estos protocolos proveen su implementación propia esta tiene precedencia, y será usada en lugar de aquellas expuestas por la extensión.

Falta aún mucho por aprender en nuestro camino a convertirnos en iOS Developer. Suscríbete a nuestra lista de correo y síguenos en nuestras redes sociales. Mantente al tanto de todas nuestras publicaciones.

Espero que todo cuanto se ha dicho aquí, de una forma u otra le haya servido de aprendizaje, de referencia, que haya valido su preciado tiempo.

Este artículo, al igual que el resto, será revisado con cierta frecuencia en pos de mantener un contenido de calidad y actualizado.

¡Cualquier duda o sugerencia, ya sea errores a corregir o ejemplos a añadir, será más que bienvenida, necesaria!

Deja una respuesta

Su dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para mejorar su experiencia. Asumiremos que está de acuerdo con esto, pero puede optar por no participar si lo desea. Aceptar Leer Más

RECIBE CONTENIDO SIMILAR EN TU CORREO

RECIBE CONTENIDO SIMILAR EN TU CORREO

Suscríbete a nuestra lista de correo y mantente actualizado con las nuevas publicaciones.

Se ha suscrito correctamente!