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:

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:

…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:

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:

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:

…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:

…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:

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:

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:

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:

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

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:

…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:

…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:

…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:

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:

…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:

…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.

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!