S.O.L.I.D Los primeros 5 principios del diseño orientado a objetos con JavaScript

Encontré un muy buen artículo que explica el S.O.L.I.D. principios, si está familiarizado con PHP, puede leer el artículo original aquí: S.O.L.I.D: Los primeros 5 principios del diseño orientado a objetos. Pero como soy desarrollador de JavaScript, he adaptado los ejemplos de código del artículo a JavaScript.

JavaScript es un lenguaje poco tipado, algunos lo consideran un lenguaje funcional, otros lo consideran un lenguaje orientado a objetos, algunos piensan que es ambos, y algunos piensan que tener clases en JavaScript es simplemente incorrecto. - Dor Tzur

Esto es solo un simple artículo de "bienvenido a S.O.L.I.D.", simplemente arroja luz sobre lo que S.O.L.I.D. es.

SÓLIDO. REPRESENTA:

  • S - Principio de responsabilidad única
  • O - Principio abierto cerrado
  • L - Principio de sustitución de Liskov
  • I - Principio de segregación de interfaz
  • D - Principio de inversión de dependencia

# Principio de responsabilidad única

Una clase debe tener una y solo una razón para cambiar, lo que significa que una clase solo debe tener un trabajo.

Por ejemplo, supongamos que tenemos algunas formas y queremos sumar todas las áreas de las formas. Bueno, esto es bastante simple, ¿verdad?

círculo const = (radio) => {
  const proto = {
    tipo: 'Círculo',
    //código
  }
  return Object.assign (Object.create (proto), {radius})
}
cuadrado const = (longitud) => {
  const proto = {
    tipo: 'Cuadrado',
    //código
  }
  return Object.assign (Object.create (proto), {length})
}

Primero, creamos nuestras funciones de fábrica de formas y configuramos los parámetros requeridos.

¿Qué es una función de fábrica?

En JavaScript, cualquier función puede devolver un nuevo objeto. Cuando no es una función o clase de constructor, se llama función de fábrica. por qué usar las funciones de fábrica, este artículo proporciona una buena explicación y este video también lo explica muy claramente

A continuación, avanzamos creando la función de fábrica areaCalculator y luego escribimos nuestra lógica para resumir el área de todas las formas proporcionadas.

const areaCalculator = (s) => {
  const proto = {
    sum () {
      // lógica para sumar
    },
    salida () {
     volver `
       

         Suma de las áreas de formas proporcionadas:          $ {this.sum ()}        

    }   }   return Object.assign (Object.create (proto), {formas: s}) }

Para usar la función de fábrica areaCalculator, simplemente llamamos a la función y pasamos una serie de formas, y mostramos el resultado en la parte inferior de la página.

formas const = [
  círculo (2),
  cuadrado (5),
  cuadrado (6)
]
const areas = areaCalculator (formas)
console.log (areas.output ())

El problema con el método de salida es que areaCalculator maneja la lógica para generar los datos. Por lo tanto, ¿qué sucede si el usuario desea generar los datos como json u otra cosa?

Toda la lógica sería manejada por la función de fábrica AreaCalculator, esto es lo que el "principio de responsabilidad única" desaprueba; la función de fábrica areaCalculator solo debe sumar las áreas de las formas proporcionadas, no debería importarle si el usuario desea JSON o HTML.

Entonces, para solucionar esto, puede crear una función de fábrica SumCalculatorOutputter y usarla para manejar cualquier lógica que necesite sobre cómo se muestran las áreas de suma de todas las formas proporcionadas.

La función de fábrica sumCalculatorOutputter funcionaría así:

formas const = [
  círculo (2),
  cuadrado (5),
  cuadrado (6)
]
const areas = areaCalculator (formas)
salida constante = sumCalculatorOputter (áreas)
console.log (output.JSON ())
console.log (output.HAML ())
console.log (output.HTML ())
console.log (output.JADE ())

Ahora, la lógica de fábrica sumCalculatorOutputter maneja cualquier lógica que necesite para enviar los datos a los usuarios.

# Principio abierto-cerrado

Los objetos o entidades deben estar abiertos para la extensión, pero cerrados para la modificación.
Abrir para extensión significa que deberíamos poder agregar nuevas características o componentes a la aplicación sin romper el código existente.
Cerrado por modificación significa que no deberíamos introducir cambios importantes en la funcionalidad existente, ya que eso lo obligaría a refactorizar una gran cantidad de código existente: Eric Elliott

En palabras más simples, significa que una clase o función de fábrica en nuestro caso, debe ser fácilmente extensible sin modificar la clase o función en sí. Veamos la función de fábrica areaCalculator, especialmente su método de suma.

sum () {
 
 área const = []
 
 para (forma de this.shapes) {
  
  if (shape.type === 'Cuadrado') {
     area.push (Math.pow (shape.length, 2)
   } else if (shape.type === 'Circle') {
     area.push (Math.PI * Math.pow (shape.length, 2)
   }
 }
 return area.reduce ((v, c) => c + = v, 0)
}

Si quisiéramos que el método de suma pudiera sumar las áreas de más formas, tendríamos que agregar más bloques if / else y eso va en contra del principio abierto-cerrado.

Una manera de mejorar este método de suma es eliminar la lógica para calcular el área de cada forma fuera del método de suma y adjuntarla a las funciones de fábrica de la forma.

cuadrado const = (longitud) => {
  const proto = {
    tipo: 'Cuadrado',
    zona () {
      return Math.pow (this.length, 2)
    }
  }
  return Object.assign (Object.create (proto), {length})
}

Se debe hacer lo mismo para la función de fábrica de círculo, se debe agregar un método de área. Ahora, calcular la suma de cualquier forma proporcionada debe ser tan simple como:

sum () {
 área const = []
 para (forma de this.shapes) {
   area.push (shape.area ())
 }
 return area.reduce ((v, c) => c + = v, 0)
}

Ahora podemos crear otra clase de forma y pasarla al calcular la suma sin romper nuestro código. Sin embargo, ahora surge otro problema, ¿cómo sabemos que el objeto pasado al areaCalculator es en realidad una forma o si la forma tiene un método llamado área?

La codificación de una interfaz es una parte integral de S.O.L.I.D., un ejemplo rápido es que creamos una interfaz que implementa cada forma.

Dado que JavaScript no tiene interfaces, voy a mostrarle cómo se logrará esto en TypeScript, ya que TypeScript modela el OOP clásico para JavaScript y la diferencia con el prototipo de OO de JavaScript puro.

interfaz ShapeInterface {
 area (): numero
}
class Circle implementa ShapeInterface {
 dejar radio: número = 0
 constructor (r: número) {
  this.radius = r
 }
 
 área pública (): número {
  return MATH.PI * MATH.pow (this.radius, 2)
 }
}

En el ejemplo anterior, se demuestra cómo se logrará esto en TypeScript, pero bajo el capó TypeScript compila el código en JavaScript puro y en el código compilado carece de interfaces, ya que JavaScript no lo tiene.

Entonces, ¿cómo podemos lograr esto, en la falta de interfaces?

Función Composición al rescate!

Primero creamos la función de fábrica shapeInterface, ya que estamos hablando de interfaces, nuestro shapeInterface será tan abstracto como una interfaz, usando la composición de funciones, para una explicación profunda de la composición, vea este gran video.

const shapeInterface = (estado) => ({
  tipo: 'shapeInterface',
  area: () => state.area (estado)
})

Luego lo implementamos en nuestra función de fábrica cuadrada.

cuadrado const = (longitud) => {
  const proto = {
    longitud,
    tipo: 'Cuadrado',
    área: (args) => Math.pow (args.length, 2)
  }
  const basics = shapeInterface (proto)
  const composite = Object.assign ({}, conceptos básicos)
  return Object.assign (Object.create (composite), {length})
}

Y el resultado de llamar a la función de fábrica cuadrada será el siguiente:

const s = cuadrado (5)
console.log ('OBJ \ n', s)
console.log ('PROTO \ n', Object.getPrototypeOf (s))
s.area ()
// salida
OBJ
 {longitud: 5}
PROTO
 {tipo: 'shapeInterface', área: [Función: área]}
25

En nuestro método de suma areaCalculator podemos verificar si las formas proyectadas son en realidad tipos de shapeInterface, de lo contrario arrojamos una excepción:

sum () {
  área const = []
  para (forma de this.shapes) {
    if (Object.getPrototypeOf (shape) .type === 'shapeInterface') {
       area.push (shape.area ())
     } más {
       lanzar un nuevo error ('este no es un objeto shapeInterface')
     }
   }
   return area.reduce ((v, c) => c + = v, 0)
}

y de nuevo, dado que JavaScript no es compatible con interfaces como los idiomas escritos, el ejemplo anterior muestra cómo podemos simularlo, pero más que simular interfaces, lo que estamos haciendo es usar cierres y composición de funciones si no sabes qué son El cierre es este artículo lo explica muy bien y para completar ver este video.

# Principio de sustitución de 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.

Todo esto indica que cada subclase / clase derivada debe ser sustituible por su clase base / principal.

En otras palabras, tan simple como eso, una subclase debería anular los métodos de la clase principal de una manera que no rompa la funcionalidad desde el punto de vista del cliente.

Todavía haciendo uso de nuestra función de fábrica areaCalculator, digamos que tenemos una función de fábrica de volumeCalculator que extiende la función de fábrica de areaCalculator, y en nuestro caso para extender un objeto sin romper los cambios en ES6 lo hacemos usando Object.assign () y Object. getPrototypeOf ():

const volumeCalculator = (s) => {
  const proto = {
    tipo: 'volumeCalculator'
  }
  const areaCalProto = Object.getPrototypeOf (areaCalculator ())
  const heredar = Object.assign ({}, areaCalProto, proto)
  return Object.assign (Object.create (heredar), {formas: s})
}

# Principio de segregación de interfaz

Un cliente nunca debería verse obligado a implementar una interfaz que no utiliza o los clientes no deberían verse obligados a depender de métodos que no utilizan.

Continuando con nuestro ejemplo de formas, sabemos que también tenemos formas sólidas, por lo que dado que también queremos calcular el volumen de la forma, podemos agregar otro contrato a la forma Interface:

const shapeInterface = (estado) => ({
  tipo: 'shapeInterface',
  area: () => state.area (estado),
  volumen: () => estado.volumen (estado)
})

Cualquier forma que creamos debe implementar el método de volumen, pero sabemos que los cuadrados son formas planas y que no tienen volúmenes, por lo que esta interfaz obligaría a la función de fábrica de cuadrados a implementar un método que no tiene ningún uso.

El principio de segregación de interfaz dice no a esto, en su lugar, podría crear otra interfaz llamada solidShapeInterface que tenga el contrato de volumen y formas sólidas como cubos, etc. pueden implementar esta interfaz.

const shapeInterface = (estado) => ({
  tipo: 'shapeInterface',
  area: () => state.area (estado)
})
const solidShapeInterface = (estado) => ({
  tipo: 'solidShapeInterface',
  volumen: () => estado.volumen (estado)
})
const cubo = (longitud) => {
  const proto = {
    longitud,
    tipo: 'Cubo',
    área: (args) => Math.pow (args.length, 2),
    volumen: (args) => Math.pow (args.length, 3)
  }
  const basics = shapeInterface (proto)
  complejo const = solidShapeInterface (proto)
  const composite = Object.assign ({}, básico, complejo)
  return Object.assign (Object.create (composite), {length})
}

Este es un enfoque mucho mejor, pero un obstáculo a tener en cuenta es cuándo calcular la suma de la forma, en lugar de usar shapeInterface o solidShapeInterface.

Puede crear otra interfaz, tal vez manageShapeInterface e implementarla en las formas planas y sólidas, de esta manera puede ver fácilmente que tiene una única API para administrar las formas, por ejemplo:

const manageShapeInterface = (fn) => ({
  tipo: 'manageShapeInterface',
  calcular: () => fn ()
})
círculo const = (radio) => {
  const proto = {
    radio,
    tipo: 'Círculo',
    área: (args) => Math.PI * Math.pow (args.radius, 2)
  }
  const basics = shapeInterface (proto)
  const abstraccion = manageShapeInterface (() => basics.area ())
  const composite = Object.assign ({}, conceptos básicos, abstracción)
  return Object.assign (Object.create (composite), {radius})
}
const cubo = (longitud) => {
  const proto = {
    longitud,
    tipo: 'Cubo',
    área: (args) => Math.pow (args.length, 2),
    volumen: (args) => Math.pow (args.length, 3)
  }
  const basics = shapeInterface (proto)
  complejo const = solidShapeInterface (proto)
  const abstraccion = manageShapeInterface (
    () => basics.area () + complex.volume ()
  )
  const composite = Object.assign ({}, conceptos básicos, abstracción)
  return Object.assign (Object.create (composite), {length})
}

Como puede ver hasta ahora, lo que hemos estado haciendo para las interfaces en JavaScript son funciones de fábrica para la composición de funciones.

Y aquí, con manageShapeInterface lo que estamos haciendo es abstraer nuevamente la función de cálculo, lo que estamos haciendo aquí y en las otras interfaces (si podemos llamarlas interfaces), estamos usando "funciones de orden superior" para lograr las abstracciones.

Si no sabe cuál es la función de orden superior, puede ir a ver este video.

# Principio de inversión de dependencia

Las entidades deben depender de abstracciones, no de concreciones. Establece que el módulo de alto nivel no debe depender del módulo de bajo nivel, sino que deben depender de abstracciones.

Como lenguaje dinámico, JavaScript no requiere el uso de abstracciones para facilitar el desacoplamiento. Por lo tanto, la estipulación de que las abstracciones no deberían depender de los detalles no es particularmente relevante para las aplicaciones de JavaScript. Sin embargo, la estipulación de que los módulos de alto nivel no deberían depender de los módulos de bajo nivel es relevante.

Desde un punto de vista funcional, estos contenedores y conceptos de inyección se pueden resolver con una función simple de orden superior, o un patrón de tipo agujero en el medio que está integrado en el lenguaje.

¿Cómo se relaciona la inversión de dependencia con las funciones de orden superior? es una pregunta que se hace en stackExchange si desea una explicación profunda.

Esto puede sonar hinchado, pero es realmente fácil de entender. Este principio permite el desacoplamiento.

Y lo hemos hecho antes, revisemos nuestro código con manageShapeInterface y cómo logramos el método de cálculo.

const manageShapeInterface = (fn) => ({
  tipo: 'manageShapeInterface',
  calcular: () => fn ()
})

Lo que recibe la función de fábrica manageShapeInterface como argumento es una función de orden superior, que desacopla para cada forma la funcionalidad para lograr la lógica necesaria para llegar al cálculo final, veamos cómo se hace esto en los objetos de formas.

cuadrado const = (radio) => {
  // código
 
  const abstraccion = manageShapeInterface (() => basics.area ())
 
 // más código ...
}
const cubo = (longitud) => {
  // código
  const abstraccion = manageShapeInterface (
    () => basics.area () + complex.volume ()
  )
  // más código ...
}

Para el cuadrado lo que necesitamos calcular es obtener el área de la forma, y ​​para un cubo, lo que necesitamos es sumar el área con el volumen y eso es todo lo que necesitamos para evitar el acoplamiento y obtener la abstracción.

# Ejemplos de código completos

  • Puedes obtenerlo aquí: solid.js

# Lecturas adicionales y referencias

  • SÓLIDOS los primeros 5 principios de OOD
  • 5 principios que te harán un desarrollador de JavaScript SOLIDO
  • Serie JavaScript SÓLIDO
  • Principios SÓLIDOS usando el mecanografiado

# Conclusión

“Si lleva los principios SÓLIDOS a sus extremos, llega a algo que hace que la Programación funcional se vea bastante atractiva” - Mark Seemann

JavaScript es un lenguaje de programación de paradigmas múltiples, y podemos aplicarle principios sólidos, y lo mejor de todo es que podemos combinarlo con el paradigma de programación funcional y obtener lo mejor de ambos mundos.

Javascript también es un lenguaje de programación dinámico y muy versátil.
Lo que he presentado es solo una forma de lograr estos principios con JavaScript, pueden ser mejores opciones para lograr estos principios.

Espero que hayas disfrutado esta publicación, actualmente todavía estoy explorando el mundo de JavaScript, así que estoy abierto a aceptar comentarios o contribuciones, y si te gustó, recomiéndalo a un amigo, compártelo o léelo de nuevo.

Puedes seguirme #twitter @ cramirez_92
https://twitter.com/cramirez_92

Hasta la próxima vez