Sunteți pe pagina 1din 12

kodigoswift.

com

Tutorial Swift - Protocolos - KodigoSwift

Josué V. Herrera
16-21 minutos

En este Tutorial Swift exploraremos tanto los protocolos como la posibilidad


de extenderlos, al mismo tiempo que descubrimos como esto puede
transformar la manera en la que escribimos código. Recuerdo que una de las
características que más me intrigaban de la versión 2 de Swift era la
posibilidad de extender los protocolos, ya que hasta ese momento esto
solamente lo podíamos hacer sobre las clases, estructuras y enumeraciones,
incluso al inicio hubieron muchos que no le encontraban sentido a esta
posibilidad.
Un protocolo define un modelo conformado por métodos, propiedades y otros
requisitos que se adapten a una tarea o una pieza de funcionalidad particular,
por lo que un protocolo puede ser adoptado por una clase, estructura o
enumeración para proporcionar una implementación real de esos requisitos.
Además se puede extender un protocolo para poner en práctica algunos de
estos requisitos o para implementar alguna funcionalidad adicional que los
tipos que lo implementen puedan aprovechar.

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

Requerimientos sobre las 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

Requerimientos sobre los 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 como 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.
Requerimientos sobre los 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:
1 protocol RandomNumberGenerator {
2    func random() -> Double
3 } // RandomNumberGenerator
4 class LinearCongruentialGenerator: RandomNumberGenerator {
5    var lastRandom = 42.0
6    let m = 139968.0
7    let a = 3877.0
8    let c = 29573.0
9    func random() -> Double {
10
11
12
13
14
15
16
17
18
19       lastRandom = ((lastRandom * a + c) % m)
20       return lastRandom / m
21    } // random
22 } // LinearCongruentialGenerator
23 class Dice {
24    let sides: Int
25    let generator: RandomNumberGenerator
26    init(sides: Int, generator: RandomNumberGenerator) {
27       self.sides = sides
28       self.generator = generator
29    } // init
30    func roll() -> Int {
31       return Int(generator.random() * Double(sides)) + 1
32    } // roll
33 } // Dice
34 var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
35 for _ in 1...5 {
36    print("Random dice roll is \(d6.roll())")
37 } // for
38
39
40
41
42
43
44
45
46
47
48
49
50
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 de protocolos
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 de protocolos
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:
1 protocol Named {
2    var name: String { get }
3 } // Named
4 protocol Aged {
5    var age: Int { get }
6 } // Aged
7 struct Person: Named, Aged {
8    var name: String
9    var age: Int
10 } // Person
11 func wishHappyBirthday(celebrator: protocol<Named, Aged>) {
12    print("Happy birthday \(celebrator.name) - you're \(celebrator.age)!")
13
14
15
16
17
18
19
} // wishHappyBirthday
20
let birthdayPerson = Person(name: "Nery", age: 15)
21
wishHappyBirthday(birthdayPerson)
22
23
24
25
26
27
28
…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.

Comprobando protocolos
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:
1 protocol HasArea {
2    var area: Double { get }
3 } // HasArea
4 class Circle: HasArea {
5    let pi = 3.1415927
6    var radius: Double
7    var area: Double {
8       return pi * radius * radius
9    } // area
10    init(radius: Double) {
11       self.radius = radius
12    } // init
13 } // Circle
14
15
16
17
18
19
20
21
22 class Country: HasArea {
23    var area: Double
24    init(area: Double) {
25       self.area = area
26    } // init
27 } // Country
28 class Animal {
29    var legs: Int
30    init(legs: Int) {
31       self.legs = legs
32    } // init
33 } // Animal
34 let objects: [AnyObject] = [Circle(radius: 2.0), Country(area: 243_610),
35 Animal(legs: 4)]
36 for object in objects {
37    if let objectWithArea = object as? HasArea {
38       print("Area is \(objectWithArea.area)")
39    } else {
40       print("Something that doesn't have an area")
41    } // else
42 } // for
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
…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

Extensiones
Los protocolos pueden ser extendidos en pos de proveer métodos y
propiedades que estén incluso más acordes a ciertos tipos ya por defecto
asociados. Esto nos permite definir comportamientos en los protocolos en sí,
en lugar de en cada tipo que lo adopte, veamos un ejemplo:
extension RandomNumberGenerator {
   func randomBool() -> Bool {
      return random() > 0.5
   } // randomBool
} // RandomNumberGenerator
…el protocolo RandomNumberGenerator puede ser extendido para
proporcionar un método randomBool, es decir un método que nos devuelve
un valor booleano aleatorio. En caso que se estén diciendo:
Esto es absurdo, ¿no sería mejor añadir randomBool junto con la definición
del protocolo?
Pues absurdo no es, ya que en la definición del protocolo ni las propiedades
ni los métodos pueden tener cuerpo y a esto se le suma el beneficio de que
podemos extender incluso protocolos propios de Swift o de terceros, es decir
protocolos no definidos por nosotros y a los que en muchos casos ni siquiera
tenemos acceso a su código fuente.
Veamos un ejemplo del segmento anterior en uso:
1 protocol RandomNumberGenerator {
2    func random() -> Double
3 } // RandomNumberGenerator
4 extension RandomNumberGenerator {
5    func randomBool() -> Bool {
6       return random() > 0.5
7    } // randomBool
8 } // RandomNumberGenerator
9 class LinearCongruentialGenerator: RandomNumberGenerator {
10    var lastRandom = 42.0
11    let m = 139968.0
12    let a = 3877.0
13    let c = 29573.0
14    func random() -> Double {
15       lastRandom = ((lastRandom * a + c) % m)
16       return lastRandom / m
17    } // random
18 } // LinearCongruentialGenerator
19 let generator = LinearCongruentialGenerator()
20 print("Here's a random number: \(generator.random())")
21
22
23
24
25
26
27
28
29
print("And here's a random Boolean: \(generator.randomBool())")
30
31
32
33
34
35
36
37
38
…este ejemplo no es nuevo, aunque sí presentamos una nueva versión del
mismo. En el podemos comprobar que no hemos modificado la definición del
protocolo ni la clase que lo adopta, lo nuevo aquí es la extensión del mismo y
la última línea donde hacemos una llamada al nuevo método añadido por la
extensión.

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 pues esta tiene precedencia y será usada en lugar de aquellas
expuestas por la extensión.

Resumen
Las extensiones de protocolos y las implementaciones por defecto pueden
lucir similar a usar base clases o incluso clases abstractas en otros lenguajes,
pero estas nos ofrecen algunas ventajas claves en Swift:
Debido a que podemos tener tipos que adopten más de un protocolo, estos
pueden ser decorados con comportamientos por defecto desde múltiples
protocolos. A diferencia de la herencia múltiple en el caso de las clases y que
Swift no soporta, la posibilidad de extender los protocolos no incluye ningún
estado adicional.
Los protocolos pueden ser adoptados por clases, estructuras y
enumeraciones, mientras que las clases base y le herencia están restringidas
a las clases.
En otras palabras, mediante las extensiones de protocolos podemos definir
comportamientos por defecto para tipos por valor (estructuras,
enumeraciones…) y no solo para tipos por referencia (clases).
Hasta donde tengo información, Swift es el primer lenguaje que introduce el
paradigma de la programación orientada a protocolos y con ello la intención
de un modelo de abstracción más eficiente y flexible, una apuesta directa por
los tipos por valor.
Falta aún mucho por aprender en nuestro camino a convertirnos en iOS
Developer. Suscríbete a nuestra lista de correo mediante el formulario en el
panel derecho y síguenos en nuestras redes sociales. Mantente así al tanto
de todas nuestras publicaciones futuras.
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 sugerencia, ya sea errores a corregir, información o ejemplos a
añadir será, más que bienvenida, necesaria!