JavaScript — Principios SOLID

Mauricio Garcia
9 min readApr 27, 2020

--

Temario

  • Introducción
  • S — Single-responsibility principle
  • O — Open-closed principle
  • L — Liskov substitution principle
  • I — Interface segregation principle
  • D — Dependency Inversion Principle

i. Introducción

Robert C. Martin[ref] estableció 5 principios de diseño orientado a objetos (ODD) básicos, para crear mejores diseños con alta cohesión y bajo acoplamiento[ref], estos cinco principios los llamó SOLID.

SOLID es un acrónimo de los primeros 5 principios de ODD, estos hacen la vida más fácil para un programador, contando con las siguientes ventajas:

  • Código mantenible.
  • Sencillo de hacer cambios y escalar.
  • Hace que su refactorización sea rápida.
  • Forman parte del desarrollo ágil.

Nota: Todos los ejemplos vamos a usar clases[ref], pero también se puede usar con funciones[ref], componentes…

ii. S — Single-responsibility principle (Principio de responsabilidad única)

Una clase o función solo debe tener una razón para cambiar.

O como dice la filosofía UnixHaz una cosa, hazla bien”, se lee bastante sencillo ¿no?, la realidad, es que es bastante complicada de implementar.

  • Debemos tener un objetivo — Nuestra clase o función solo debe hacer exactamente una cosa.

Lo interesante es poder definir “una cosa”, y se vuelve aún más interesante “que única cosa”…

:: Ejemplo 1

Imagina que tenemos una aplicación, donde se compone de una sola función llamada initApp.

¿cumplimos que haga una sola cosa al llamarla initApp? — Quizás sí, y esa cosa sería una MUY GRANDE.

Pero…

  • ¿está bien hacer una GRAN cosa? —
  • ¿cómo saber si estamos bien o mal? —
  • ¿En qué momento debemos separarlo? —

Se que son muchas preguntas, para las preguntas anteriores, Dor Tzur[ref], tiene las siguientes estrategias:

  1. Revisar el nombre de sus funciones, si su descripción hace más de una cosa, quiere decir que probablemente estamos rompiendo con el principio de responsabilidad única.
  2. Para cada función creada, revisar si hay una parte que pueda extraerse en una función aún más pequeña.
  3. Revisar nuevamente y de todas revisar cuantas funciones pueden ser reutilizables.

Supongamos que dentro de la función initApp tenemos la función loginUserAndGetGroups, claramente estamos rompiendo con el primer punto.

Vamos a complicarlo un poco más…

Imagina que cada vez que el usuario se loguea, se debe obtener: Películas, series de tv y música favorita. Siguiendo el principio, lo ideal sería generar un método para cada uno : getMovies, getShows, getMusic, pero qué pasaría, si esas 3 funciones se mandan a llamar en otros lados, NO estaría bien generar una función getMoviesAndShowsAndMusic, pero SI podemos generar una nueva función getUserMedia que encapsule las 3 funciones (que a su vez son funciones puras e independientes).

Entonces podemos decir que usar initApp no esta mal, siempre y cuando su implementación interna está dividida de manera correcta.

:: Ejemplo 2

Imagina que tenemos una aplicación que muestra un catálogo de carros donde tenemos un método que hace el calculo de cuanta gasolina gasta y otro donde nos traiga las características que tiene.

Pero resulta que quieren que cuando sean carros eléctricos y carros de F1, la forma de calcular la gasolina sea diferente, quizás, la solución que se te viene a la mente es así:

Quizás como una solución temporal pueda ser aceptable, pero como LA SOLUCIÓN , no es la ideal, ya que en un solo método tenemos la lógica de todos los posibles tipo de carros y hacer modificaciones es seguro que nos llenemos de if, switch, por lo que lo ideal sería hacer nuevas clases por tipo de carro y extender de Car.

:: Ejemplo 3

iii. O — Open-closed principle (Principio de abierto cerrado)

Los objetos o entidades deben estar abiertos para la extensión, pero cerrados para la modificación.

:: Ejemplo 1

Como podemos observar, no tenemos forma de agregar una fruta nueva sin modificar fruits, entonces:

Se ha agregado un nuevo método addFruit, donde podemos agregar nuevas frutas, y con esto se ha cumplido con el principio de abierto-cerrado, ya que se han hecho modificaciones a la clase, pero, sin alterar la funcionalidad original.

:: Ejemplo 2

Intentar llevar a cabo este principio al 100% es básicamente imposible, ya que es muy complicado prever todos los posibles casos que pueda tener, por lo que es aconsejable solo aplicarlas cuando realmente sea necesario, ya que muchas veces puede llegar a complicar el código, tan así, que se puede volver muy difícil de mantener.

iv. L — Liskov substitution principle (Principio de sustitución Liskov)

Sea q(x) una propiedad demostrable sobre objetos de x de tipo T. Entonces q(y) debería ser demostrable para objetos y de tipo S donde S es un subtipo de T.

Bueno… ehm… si alguien entendió lo que está arriba, que levante la mano…

En palabras más simples, el principio nos dice: si estás usando una clase y esta es extendida, debes de poder utilizar cualquier clase hija, y que la aplicación siga funcionando sin problemas, o mejor aún, que las pruebas del padre sigan siendo válidas

Veamos un ejemplo de dónde y cómo podemos aplicarlo…

:: Ejemplo 1

Expliquemos el ejemplo más común, imagina que tenemos la clase Rectangle:

Si ejecutamos nuestro test con Mocha[ref], quedaría algo así:

Imagina, que ahora nos piden que hagamos la clase Square, ya que la fórmula es CASI la misma, podríamos extender de Rectangle, algo así (haciendo algunos ajustes):

Genial, hemos reutilizado clases y nos hemos ahorrado código, pero, que pasa si hacemos los test:

Al ejecutar el test no va a pasar, ya que el resultado va a ser 4 en vez de 16, por lo que estamos faltando al principio de Liskov.

Una forma de solucionarlo es hacer una clase genérica Parallelogram:

Después, las clases Rectangle y Square, quedarían de la siguiente manera:

Cuando hagamos una instancia de objeto de alguna de las clases, aprovechamos el constructor para mandarle los datos necesarios, y con este cambio no estaremos afectando al padre Parallelogram al momento de hacer las pruebas.

:: Ejemplo 2

El apocalipsis ha llegado y los Zombies nos están invadiendo!!!, por lo que debemos de saber que están haciendo a todo momento…

Imagina que tenemos una clase Zombie, donde vamos a interactuar y saber que es lo que está haciendo:

Veamos cómo funciona:

Si hacemos nuestras pruebas:

Perfecto, sabemos a todo momento cuales son los pasos de los Zombies, por lo que no debemos preocuparnos… o eso creíamos…resulta que los Animales también se están infectando y son 3 veces más rápidos que los humanos!!…

Quizás lo que estés pensando es hacer una clase llamada ZombieAnimal y en ella cambiar el método speedUp:

Puede se que funcione, pero… al momento de hacer las pruebas, vemos un error:

Cuando se hace la comparación realmente marca error, ya que solo estamos considerando la velocidad de aceleración del zombie pero no la velocidad de desaceleración.

Tu deber es sobrevivir…¿Cómo lo resolverías?…🧟‍♀️ 🧟‍♂️

:: Ejemplo 3

v. I — Interface segregation principle (Principio de segregación de interfaz)

Los clientes no deben ser forzados a depender de los métodos que no utilizan.

Nos dice que ninguna clase debería depender de métodos que no usa, cuando creamos clases, es importante que las interfaces que se implementen, SIEMPRE se van a ocupar, y que también, se puedan agregar nuevos comportamientos a todos los métodos.

Si observas con cuidado, el párrafo anterior, es muy similar al principio de responsabilidad única, ya que ambos se centran en la cohesión de responsabilidades.

Bueno y — ¿qué pasa si no podemos cumplir con este principio? — , lo ideal sería al menos tener interfaces más pequeñas, veamos un ejemplo para entenderlo mejor.

:: Ejemplo 1

Imagina que tenemos una tienda, donde manejamos el stock de todos nuestros productos, entonces, tenemos la clase Product, algo así:

La clase Product tiene propiedades que son útiles para cualquier producto. Tenemos la clase Milk, si la implementamos, podemos observar que hace uso de todos los métodos.

Nuestra tienda está creciendo, vendemos bien, y ahora decidimos vender alcohol… como bien sabemos, no podemos vender a cualquier persona, tiene ciertas restricciones (debe ser mayor de edad), quizás se te ocurrió, agregar una validación a la clase Product:

Puede ser una buena solución, pero… el problema es que todos los productos que no necesitan esa información estarán obligados a implementarla, quizás, podríamos ponerle un “parche” a todas aquellas que no la requieran:

En este caso lo que hicimos en la clase Milk fue sobreescribir el método getRecommendedAge, para que, sin importar la edad siempre se pueda vender…creo que no es necesario decir que eso está MUY MAL, ya que estamos agregando dependencias y código innecesario, ya que cada que extendamos la clase Product, y el producto no aplique, estaríamos obligados a sobreescribir.

Sabemos que es al momento de vender alcohol donde se quiere hacer la validación, entonces quizás podemos poner esa dependencia dentro de la clase Beer:

Bien, creo que tiene más sentido…hemos resuelto nuestro problema, pero qué pasaría si algún otro producto también requiere validar si es para mayores de edad…la solución inmediata es copiar y pegar la funcionalidad en todos los lados posibles… imagina si hay cambios uff

Así que si vemos un poco más allá, la otra solución puede ser separar en interfaces más pequeñas, para que así cada clase extienda lo que necesite.

Importante: En ES6 solo se puede tener una clase padre, por lo que la herencia múltiple de manera directa no es posible, para eso se usan los mixins. [ref]

Entonces, tenemos nuestra clase Product

Ahora generamos el mixin AgeAware:

Ahora, en la clase Beer :

En el caso de la clase Milk, no va a ser afectada:

La ventaja, es que el mixin AgeAwareMixin podemos extenderlo en todas las clases que apliquen la regla:

:: Ejemplo 2

vi. D — Dependency Inversion Principle (Principio de inversión de dependencia)

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

B. Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.

Los componentes principales de la aplicación no deben depender de los detalles de la implementación (framework, base de datos, conexiones, etc.) ya que todos estos aspectos se van a especificar mediante interfaces, así los componentes no tendrán que preocuparse por cómo o donde esté implementado.

:: Ejemplo 1

Imagina que tenemos una app donde le mostramos al usuario tiendas cercanas a su ubicación, entonces tendríamos la clase donde va a conectarse a una base de datos, obtener los puntos y pintar en el mapa.

Con este ejemplo estamos rompiendo todas las reglas que hemos venido hablando, y que una clase de alto nivel PaintMap está dependiendo de otras clases de alto nivel Promise, Map.

Qué pasaría si en vez de promesas nos piden usar await/async y en vez de GoogleMaps nos piden usar algún otro…

Debemos crear interfaces que definen el comportamiento que debe dar una clase para poder funcionar (obtención de datos y pintado de mapa):

Ya una vez que hemos separado nuestras interfaces, ahora lo que debemos hacer, es pasar estos objetos por medio del constructor.

Si nos damos cuenta a la clase PaintMap, ya no le importa cómo va a obtener los datos, así como tampoco que API de mapa va a usar; con esto hemos desacoplado nuestras dependencias; y lo mejor es que al momento de hacer pruebas con PaintMap podemos exponerle diversos casos de uso sin temor a que falle.

:: Ejemplo 2

En la siguiente entrega vamos a ver JavaScript — Introducción a los patrones de diseño en JS

La entrega pasada vimos HTML/JavaScript — Document Object Model (DOM)

--

--

No responses yet