Un Flutter Más Limpio Vol. 2: SOLID Rules en Dart

Elian Ortega
12 min readJan 11, 2021

--

📗 Recomendación: Volver a este artículo las veces que sea necesario para aclarar los siguientes volúmenes, ya que hay conceptos que se complementan con estos. Vol 1. Un Flutter Más Limpio: Intro a CLEAN

Después de haber iniciado en el mundo de la programación todos llegamos a un punto en el cual tenemos que ver atrás en el camino y revisar algunas de las líneas de código que hemos escrito, ya sea hace 1 día para recordar alguna idea o de hace años para revisar la implementación de algún modulo de nuestro software.

Muchas veces en estas ojeadas al código del pasado nos encontramos con una lista de problemas como:

  • Tener que buscar entre muchos archivos la respuesta a lo que buscamos.
  • No entender el código que escribimos.
  • Invertir demasiado tiempo en resolver un bug pequeño.

Estos problemas surgen desde que iniciamos un proyecto debido a que no dedicamos el tiempo suficiente para tener una idea clara no de solo de lo que vamos a hacer, pero también de cómo lo vamos a hacer.

Tenemos que desarrollar código imaginándonos lo que ocurriría si vuelvo en 2 años para revisarlo, esta habilidad de programar código limpio y entendible es primordial para facilitar el desarrollo, especialmente si se trabaja en equipo.

¿Qué es SOLID?

SOLID es el acrónimo para un conjunto de principios que nos ayudan a desarrollar un código más mantenible que también permita su extensión sencilla sin comprometer el código ya desarrollado.

En otras palabras…

Escribir más código sin dañar lo que ya funciona.

Las podemos ver incluso como un conjunto de guias para desarrollar mejor código.

Ahora vamos a explorar cada uno de los principios, que pueden ser aplicados a cualquier lenguaje de programación, pero yo los voy a cubrir desde Dart ya que es el lenguaje que utilizamos al desarrollar con Flutter.

Antes de seguir es importante destacar que estas fueron primeramente introducidas por Uncle Bob les dejo un link por si quieren ver su explicación: SOLID Principles Uncle Bob.

S: Single Responsibility Principle — Principio de Responsabilidad Única

Una clase debe de tener una, y solo una, razón para cambiar.

Para explicar este principio podemos imaginarnos un habitación desordenada, como todos la hemos tenido en algún momento, tal vez incluso ahora que estás leyendo esto.

Pero la verdad es que dentro de esta todo tiene su lugar y todo debería de estar en su lugar designado para que el orden exista.

Para dejar de un lado la analogía, este principio nos dice más específicamente:

  • Una clase debe de tener una responsabilidad única (aplica a métodos, variables, entidades, etc).
  • Existe un lugar para todo y todo debe de estar en su lugar.
  • Todas las variables y métodos de la clase deben de estar alineados con el objetivo de la misma.

Al cumplir estos principios logramos clases más pequeñas y sencillas que tiene objetivos únicos por los que evitamos clases gigantes con propiedades y métodos genéricos que pueden ser redundantes en el desarrollo.

Veamos un ejemplo:

Veamos esta función signUp(). Este podría ser el método que llamamos desde nuestra capa de UI para realizar el proceso de sign-up del usuario.

En el código vemos que hay funcionalidad de creación, validación, conversión a JSON, e incluso el llamado a la base de datos que normalmente es un llamado a API, por lo que claramente no estamos cumpliendo el principio.

Este es uno de los errores más comunes, especialmente en Flutter, ya que los desarrolladores fácilmente cometemos el error de combinar diferentes cosas dentro de la misma clase o métodos.

🚨 En otros artículos y videos veremos cómo se aplica esto a Clean Architecture….

Listo, ya lo entendí, ahora… ¿cómo lo aplico?

Para cumplir el Principio de Responsabilidad Única, podríamos crear métodos y clases con funcionalidades sencillas y únicas.

En el método de ejemplo signUp() se hacen muchas cosas con diferentes objetivos, cada una de estas funcionalidades podría ser separada en una clase individual con objetivo único.

La validación

Podríamos implementar una clase que se encargue de realizar la validación, hay muchas maneras de hacer esto, una de ellas puede ser utilizar formz que es un package de Flutter que nos permite crear clases para un tipo de dato y realizar una validación para el mismo. Estos son luego utilizados desde nuestra lógica de negocio para realizar validaciones.

No se concentren mucho en la lógica del código, lo importante es entender ver que ahora la validación se encuentra desacoplada del resto de la lógica con el método validator().

Aquí les dejo el link a formz en pub.dev.

La conexión con la fuente de datos

El otro error es realizar llamados un API desde la lógica de negocios o UI, esto no cumpliría el principio ya que la conexión con el API es una funcionalidad compleja por si misma por lo que lo mejor seria implementar una clase como repositorio a la cual le pasemos los parámetros que vamos a enviar y delegar el resto del proceso la misma.

Este concepto de repositorio es clave para cumplir con los estándares de Clean Architecture pero podemos ver que el principio de Responsabilidad Única es el que esta detrás de toda esta idea.

Al implementar estas clases aplicando el principio lograríamos un método más sencillo comparado a como lo iniciamos:

Más sencillo, mantenible y desacoplado.

O: Opened/Closed Principle — Principio de Abierto/Cerrado

Una entidad debe ser abierta para extenderse, pero cerrada para modificarse.

Este principio nos dice, en resumen, que debemos extender de la entidad para agregar nuevo código en vez de modificar el existente.

La primera vez que leemos esto puede ser un poco confuso pero simplemente es:

No modifique lo que ya funciona, solo extienda y agregue nuevo código

De esta manera, logramos desarrollar sin dañar el código previamente probado. Para entender este principio podemos ver un ejemplo brindado por Katerian Trjchevska en LaraconEU 2018.

Imaginemos que nuestra app posee un módulo de pagos que actualmente solo acepta como métodos de pago tarjetas de débito/crédito y PayPal.

En un primer vistazo del código podemos pensar que todo está bien, pero cuando analizamos su escalabilidad, nos damos cuenta del problema.

Imaginemos que nuestro cliente nos solicita agregar más métodos de pago como AliPay, tarjetas de regalos y otros.

Cada nuevo método de pago implica una nueva función y un nuevo else if en el método pay() y podríamos decir que esto no es un problema, pero de seguir agregando código dentro de la misma clase, nunca lograríamos un código estable y listo para producción.

Al aplicar el principio abierto/cerrado, podemos crear una clase abstracta PayableInterface que nos sirva como interfaz de pago, de esta manera cada uno de nuestros métodos de pago extiende esta clase abstracta [NombreMétodoPago] extends PayableInterface y puede ser una clase independiente que no se vea afectada por modificaciones realizada en otra.

Luego de tener nuestra lógica de pago implementada, podemos recibir como parámetro que nos permita seleccionar el PayableInterface indicado para la transacción y de esta manera no tenemos que preocuparnos de cómo el método pay() realiza el pago, solamente de realizar un tipo de filtrado para que se utilice la instancia correcta de la interfaz; ya sea Card, PayPal o Alipay.

Al final tendríamos un método como este en donde podemos notar que se redujo el código a solo 3 líneas y es mucho más fácil de leer.

También es más escalable ya que si quisiéramos agregar un nuevo tipo de método de pago solo tendríamos que extender de PayableInterface y agregarlo como una opción en el método de filtrado.

👆🏼 Sé que estos conceptos de abstracciones e instancias son confusos al inicio pero a lo largo de esta serie de articles y la practica les prometo se vuelven conceptos sencillos.

L: Liskov Substitution Principle — Principio de sustitución de Liskov

Podemos cambiar cualquier instancia concreta de una clase con cualquier clase que implemente la misma interfaz.

El objetivo principal de este principio es que siempre deberíamos de obtener el comportamiento esperado del código sin importar la instancia de clase que se este utilizando.

Para poder cumplir este principio de manera correcta hay 2 partes importantes:

  • Implementación
  • Abstracción

La primera la podemos ver en el ejemplo anterior de Principio Abierto/Cerrado cuando tenemos PayableInterface y los métodos de pago que la implementan como CardPayment y PaypalPayment.

En su implemetación del código vemos que no importa cuál de estas utilicemos, nuestro código debería de seguir funcionando correctamente y esto es porque ambas cumplen con la implementación de la interfaz PayableInterface.

Pero con este ejemplo es fácil de entender, en la práctica hay muchas veces que realizamos mal el proceso de abstracción, por lo que no logramos verdaderamente cumplir con el principio.

Si no estás muy familiarizado con conceptos como el de interfaz, implementación, y abstracción esto puede sonar un poco complejo pero veámoslo con un ejemplo sencillo.

Este es una de las imágenes icónicas del principio ya que facilita su comprensión.

Imaginemos que en nuestro código tenemos una clase llamada DuckInterface.

Esta nos brinda la funcionalidad básica de un pato como fly, swim, quack y tendríamos la clase RubberDuck que implementa la interfaz.

En una primera instancia podríamos decir que nuestra abstracción esta bien ya que estamos utilizando una interfaz que nos da la funcionalidad que necesitamos, pero el método fly() no aplica para un pato de goma, incluso imaginemos que nuestro programa va a tener diferentes animales con funcionalidad como la de volar y nadar compartida por lo que no tendría sentido dejar esta dependiente de la interfaz DuckInterface.

Espero ya para este punto logren ver a lo que me refiero.

Para resolver esto y cumplir con el Principio Liskov podemos crear interfaces más especificas que nos permitan la reutilización de código, lo que hace que nuestro código se más mantenible.

Con esta implementación, nuestra clase RubberDuck sólo implementa los métodos que necesita y ahora, por ejemplo, si necesitáramos un animal que cumpla una función especifica como nadar podríamos utilizar cualquiera que implemente la interfaz SwimInterface, ya que al cumplir el Principio Liskov podemos sustituir por cualquier clase sin que implemente la interfaz y esto no tendría ninguna repercusión en la funcionalidad del código.

I: Interface Segregation Principle — Principio de segregación de interfaces

El código no debe depender de métodos que no usa.

Este se puede decir que es el principio más sencillo pero por esto mismo en un inicio nos puede llegar incluso a confundir.

En los principios anteriores hemos visto la importancia del uso de las interfaces para desacoplar nuestro código.

Este principio se asegura que nuestras abstracciones para la creación de interfaces sean las correctas, ya que no podemos crear una nueva instancia de una interfaz sin implementar uno de los métodos definidos por las mismas. Lo anterior estaría incumpliendo el principio.

Esta imagen muestra el problema de no cumplir este principio, tenemos algunas instancias de clases que no utilizan todos los métodos de la interfaz teniendo así un código sucio y una mala abstracción.

Es más sencillo verlo con el típico ejemplo de animales, este se parece mucho al ejemplo que vimos de Liskov.

💡 Les destaco que en este punto los ejemplos se vuelven similares pero lo que se busca es que vean el código de otra perspectiva.

Tenemos una clase abstracta Animal que es nuestra interfaz, esta tiene definido 3 métodos eat(), sleep(), y fly().

Si creamos una clase Bird que implemente la interfaz animal no vemos ningún problema, pero ¿qué pasa si queremos crear la clase Dog?

Exacto, nos damos cuenta que no podemos implementar el método fly() porque este no aplica para un perro.

Podríamos dejarlo así y evitarnos el tiempo de reestructurar el código ya que lógicamente sabemos que esto no afectaría nuestro código, pero esto rompe el principio.

El error se comete al tener una mala abstracción en nuestra clase y lo correcto siempre es refactorizar para asegurarnos que los principios se están cumpliendo.

Puede que en el momento nos tome un poco más de tiempo pero los resultados de tener un código escalable y limpio siempre deberían de ser prioridades.

Una solución a esto es que nuestra interfaz Animal sólo posea los métodos compartidos por los animales como eat(), sleep() y nosotros creemos otra interfaz para el método fly(). De esta manera, sólo los animales que necesiten este método implementan su interfaz.

🔥 ¡Ya falta poco! Último principio SOLID…

D: Dependency Inversion Principle — Principio de Inversión de dependencias

Los módulos de alto nivel no deben depender de los modelos de bajo nivel. Ambos deben depender de abstracciones.

En mi opinión, este debería de ser el primero que todo desarrollador debe entender.

Este principio nos dice:

  • Nunca hay que depender en una implementación concreta de una clase, sólo en sus abstracciones (interfaces).
  • Igual que la imagen presentada en el volumen 1 de esta serie de Un Flutter Más Limpio, seguimos la regla que los módulos de alto nivel no deben depender estrictamente en los módulos de bajo nivel.

Para entenderlo de manera más sencilla, veamos el ejemplo.

Hoy en día cualquier app o software que se desarrolle necesita estar comunicado al mundo exterior. Normalmente esto se hace por medio de repositorios de código que instanciamos y llamamos desde la lógica de negocios en nuestro software.

El declarar y utilizar una clase concreta como es en este caso el DataRepository() dentro de BusinessLogic(), es una práctica muy común y es uno de los errores que hace que nuestro código sea poco escalable porque al depender de una instancia concreta de código, esta nunca será estable al constantemente estar agregando código en ella.

Para resolver este problema, el principio nos dice que hay que crear una interfaz que comunique ambos módulos. Incluso Se puede desarrollar la lógica de negocios sin depender que el repositorio esté implementado.

Esto también permite mejor comunicación en un equipo de desarrolladores porque al crear una interfaz, todos están claros de los objetivos del módulo y desde esa definición, se puede verificar que se estén cumpliendo los principios SOLID.

Con esta implementación, creamos un DataRepositoryInterface que luego implementamos en DataRepository y la magia ocurre dentro de la clase que utilice esta funcionalidad cuando no dependemos de una instancia concreta, sino de una interfaz, por lo que podríamos en este caso pasar como parámetros cualquier clase concreta que implemente esta interfaz.

Podría ser una base de datos local o externa y eso no afectaría el desarrollo ya que lo repito nuevamente no dependemos de una sola instancia concreta, podemos utilizar cualquier clase que cumpla con la implementación de la interfaz.

Y esto, damas y caballeros, es lo que nos permite cumplir con la palabra mágica del Clean Architecture: ¡Desacoplamiento!

Para terminar…

Le recuerdo que esto son principios no reglas por lo que no hay una manera única de seguirlos, su uso y cumplimento en el código dependerán cada proyecto ya que para muchos de estos el objetivo del proyecto es determinante para tomar decisiones. Así como algo dentro del alcance en un proyecto puede ser considerado pequeño puede que bajo otros requerimientos se convierta en algo grande.

Espero ahora tengan una mejor idea de que son los principios SOLID y cómo aplicarlos. Para cualquier duda o comentario pueden contactarme por mis redes sociales y si aprendieron algo no duden en compartirlo con sus compañeros y amigos desarrolladores, para todos como comunidad sigamos mejorando y desarrollando proyectos escalables de alta calidad.

Además, si te gustó este contenido, podés encontrar aún más y seguir en contacto conmigo en mis redes sociales:

GitHub, LinkedIn, Twitter, YouTube.

Originally published at http://github.com.

--

--

Elian Ortega
Elian Ortega

Written by Elian Ortega

I focus on writing high-quality, scalable, and testable applications. I like to write articles and make videos about tech.

Responses (2)