Saltar al contenido
KodigoSwift

Tutorial Swift – Composición vs Herencia

Composición vs Herencia

En el Tutorial Swift de hoy abordaremos un tema bastante polémico. Aprenderemos sobre la composición y la herencia en el lenguaje Swift. Analizaremos mediante ejemplos los aspectos positivos y negativos de estos principios de la programación orientada a objetos.

Ante todo tenemos que abrirnos a la posibilidad de que ciertos pilares no siempre resultan igual de óptimos y que en efecto todo es perfectible a favor de un código más limpio, elegante y flexible. Por suerte para nosotros, Swift es un híbrido entre la programación imperativa y la declarativa, un lenguaje bastante flexible que se adapta como anillo al dedo para todo cuanto queramos explorar e innovar.

Herencia

La herencia nos permite que una clase base herede los estados y comportamientos que conforman a una clase padre, veamos:

En este ejemplo creamos una clase base de nombre Perro y cuando decimos base ya estamos indicando que esta fungirá en representación de todos los perros, algo así como un template que define las características comunes a todas las instancias de este animal. En el siguiente bloque creamos otra clase de nombre Chihuahua en representación a esta raza de perros y heredamos de nuestra clase base Perro, con esto último ya garantizamos a nuestra raza toda la herencia asociada al hecho de ser un perro por lo que cada nuevo objeto (perro) podrá ladrar de manera automática.

Como podemos percatarnos la herencia es de bastante utilidad a la hora de modelar jerarquías como la anterior. Sin embargo la herencia no está exenta de limitantes. Modifiquemos un poco el ejemplo anterior:

Hemos añadido dos razas más de perros, el Golden Retriever y el Pastor Belga. Ambos ladran al igual que Arturito pero a diferencia del Chihuaua son razas que destacan por su buen olfato, el cual los hace ideales para el control de narcóticos. La jerarquía de nuestras clases actualmente luce así:

Jerarquía de Clases - Primera Aproximación

Así que hemos declarado la función olfatearDroga en Pacheco y en Firulais, la salida en pantalla es:

Pero habíamos dicho que no todo es color de rosas en la herencia, entonces:

¿Qué hay de malo con el código anterior?

Pues resulta que hemos duplicado la función olfatearDroga y esto hay que evitarlo (recordad el DRY (Don’t repeat yourself (No te repitas))). Quizás haya alguno que diga que esto ha ocurrido por un error de diseño ya que olfatearDroga es una característica de los perros por lo que pudiéramos añadirla a la clase base Perro y fin de la historia. Pues resulta que olfatearDroga sí es una característica pero de algunos perros, no de todos. Así que esta solución la clasificaría como válida a medias y no:

  • Válida a medias: ya que solucionaríamos el código duplicado pero…
  • No: debido a que la raza Chihuahua no es buena olfateando droga y nos veríamos obligados a implementar la función olfatearDroga en la clase Chihuahua, lanzando una excepción en caso de ser llamada o bien dejándola vacía ya que no la vamos a usar, lo que es una chapuza terrible y una evidencia más sobre el problema de diseño ante el cual nos encontramos. Esto también violaría el Principio de Sustitución de Liskov y el Principio de Segregación de Interfaces de SOLID ya que la clase Perro comprendería particularidades que no son comunes a todos los casos, estaríamos sin dudas añadiendo demasiadas características a una misma clase base. Exacto, solamente una característica innecesaria que añadamos ya es demasiado.

Ahora, una solución también sería crear dos subclases que dividan la familia de los perros en dos grupos, aquellas razas que son muy buenas olfateando droga y aquellas que no lo son tanto. A partir de aquí continuaría la jerarquía de nuestras clases, tal y como luce el siguiente diagrama:

Jerarquía de Clases - Segunda Aproximación

Este enfoque resuelve el problema del código duplicado al mismo tiempo que añade más complejidad y aún así no es una solución viable a nivel de arquitectura ya que no es escalable.

Imaginemos que necesitamos representar las razas grandes, su buena actitud para el adiestramiento, los cuidados que deben recibir, las enfermedades a las que son propensos y otras características en general. En este grupo entran los Golden Retriever y los Pastores Belga pero si declaramos en cada una estas clases la definición de que estas pertenecen a las razas grandes y sus características comunes pues estaremos cayendo nuevamente en código duplicado.

¿Cuál sería la respuesta correcta ante esta nueva necesidad?

Quizás tu solución venga motivada por la del diagrama anterior, y tu enfoque sea añadir otra clase por ejemplo de nombre RazasGrandes en plan:

Jerarquía de Clases - Tercera Aproximación

En este diagrama de ejemplo hemos añadido junto a la clase RazasGrandes la clase CockerSpaniel en representación de las razas medianas donde también hay perros muy buenos olfateando narcóticos. Así que “supuestamente” nuestro problema de código duplicado ya ha sido resuelto en las clases GoldenRetriever y la PastorBelga al heredar de las clases PerrosNarcoticos y RazasGrandes, cierto?

Pues NO amigos, este enfoque es errado ya que Swift no permite herencia múltiple y si lleváramos a código el diagrama anterior:

…obtendríamos el siguiente mensaje de error por parte del compilador:

Partiendo por el evidente aumento en la complejidad y por el hecho objetivo de que estamos limitados por la herencia múltiple, nuestra salida de éxito aparente sería la duplicidad en el código con los problemas de mantenimiento a futuro que esto nos traería consigo. Es decir, el enfoque de solucionar este problema usando la herencia no es escalable y ni medianamente óptimo.

Composición

Cuando hablamos de la composición sobre la herencia o quizás, mejor dicho, la composición como una alternativa a la herencia nos basamos en las limitantes antes comentadas y en la necesidad de lograr un sistema más flexible y escalable. Al abogar por la composición perseguimos también ese pilar de la programación orientada a objetos que es el polimorfismo al mismo tiempo que la reusabilidad del código.

Para lograr todo esto usualmente comenzamos por dividir los comportamientos de nuestro sistema en interfaces o en el caso de Swift pues en protocolos, encapsulando así la implementación de estas responsabilidades en aquellos objetos que realmente las vayan a necesitar.

Creo que mediante ejemplos se entenderá mejor, veamos:

Claramente hemos sustituido la clase Perro por un protocolo de igual nombre, donde se declara el uso de la función ladrar que luego todas las clases o estructuras que lo implementen tendrán que definir. En este momento, muchos de ustedes estarán diciendo algo similar a:

¡Pero que me estás contando chaval si has vuelto a duplicar el código!

Pues aún no acabamos, a partir de la versión 2 de Swift podemos declarar comportamientos por defecto a los métodos que forman parte de los protocolos mediante una extensión, por lo que el código correcto sería:

…la salida en pantalla sería la esperada:

Si deseemos modificar el ladrido de alguna de estas razas solamente tendríamos que sobrescribir el método ladrar y especificar el matiz deseado.

Ahora, todavía necesitamos representar a los perros que pueden olfatear droga y aquellas razas grandes con sus características. Pues resulta que como las clases pueden adoptar múltiples protocolos el siguiente código es completamente válido:

…aunque me surge una pregunta:

¿Tiene sentido usar una clase en estos ejemplos?

Pues en el ámbito en el que nos estamos moviendo y dada la naturaleza de los ejemplos pues no veo que tenga sentido seguir usando clases. Si a esto le sumamos el enfoque de Swift por las estructuras y que estamos abogando por la composición pues yo diría que el duo perfecto (en este caso) sería el conformado por estructuras y protocolos.

Dicho esto la versión final de nuestro código sería:

…la salida en pantalla es:

Ahora ya podemos representar perros de diferentes razas, características y comportamientos sin código duplicado y sin terminar limitados por una jerarquía rígida.

Polimorfismo

Una de las grandes ventajas de la programación orientada a objetos y otro de sus pilares es la posibilidad de usar varios objetos a través de una misma interfaz mientras que cada uno de estos se comportan de forma única.

En una clínica veterinaria entran todos los perros, así que de la línea 13 a la 21 verificamos si nuestro objeto además de ser un perro es de raza grande ya que en caso contrario pues el protocolo de admisión quizás sea otro. Una alternativa al método addPaciente pudiera ser:

…donde hacemos uso de genéricos y mediante la cláusula where filtramos por el caso específico que deseamos tratar.  Luego hacemos:

…y obtenemos:

Pero en el caso de intentar hacer uso de este método y agregar un perro que no sea una raza grande, tal y como:

…obtenemos el siguiente mensaje de error:

Esto evidentemente es un ejemplo trivial y completamente enfocado en demostrar que a través de protocolos y estructuras también podemos lograr polimorfismo. Si algo del código anterior no les resulta familiar los invito a visitar el artículo que hemos escrito sobre los genéricos en Swift.

Conclusiones

Cuando diseñamos nuestro código alrededor de la herencia la jerarquía que se genera está claramente centrada en lo que estos objetos son, en lo que representa cada clase y la relación entre estas. Muchas veces se esquematiza de maneras tan cerradas que entre los iOS Developer con más experiencia hay una reflexión bastante jocosa y exagerada (pero bien acertada) donde en un sistema pedimos una banana y obtenemos la banana, al mono que las sostiene y la selva o el zoológico completo junto a estos… ya que estos objetos se han conceptualizado en plena dependencia, no se ha manejado la posibilidad de que el mono pueda estar fuera de la selva o el zoológico, ni tampoco de que un plátano tenga sentido de ser en la selva o en el zoológico más allá de que el mono que ahí vive lo sostenga y así de igual manera con la selva y el zoológico.

Por todo esto es que decimos que la jerarquía que se genera mediante la herencia está limitada por lo que los objetos son, mientras que cuando abogamos por la composición nos encontramos ante un modelo mucho más flexible donde podemos diseñar un objeto componiéndolo de las varias características que deseamos que este tenga, características que son reutilizables en otras entidades incluso bien distintas como por ejemplo pudiera ser el acto de caminar o de desplazarse en el espacio tiempo. Un protocolo movimiento puede ser acoplado a un perro, a un ave, a un humano como a un avión. Nuevamente amigos la herencia se centra en lo que los objetos son mientras que la composición se enfoca en lo que estos hacen.

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!

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!