Al crear aplicaciones JavaScript, puedes encontrarte con situaciones en las que necesites crear objetos de una forma determinada y predefinida, o reutilizar una clase común modificándola o adaptándola a múltiples casos de uso.

Por supuesto, no es conveniente resolver estos problemas una y otra vez.

Aquí es donde los patrones de diseño de JavaScript vienen a tu rescate.

Los patrones de diseño de JavaScript te proporcionan una forma estructurada y repetible de abordar los problemas más frecuentes en el desarrollo de JavaScript.

En esta guía veremos qué son los patrones de diseño de JavaScript y cómo utilizarlos en tus aplicaciones JavaScript.

¿Qué Es un Patrón de Diseño de JavaScript?

Los patrones de diseño de JavaScript son plantillas de soluciones repetibles para problemas frecuentes en el desarrollo de aplicaciones JavaScript.

La idea es sencilla: Los programadores de todo el mundo, desde los inicios del desarrollo, se han enfrentado a conjuntos de problemas recurrentes al desarrollar aplicaciones. Con el tiempo, algunos desarrolladores decidieron documentar formas probadas de abordar estos problemas para que otros pudieran consultar las soluciones con facilidad.

A medida que más y más desarrolladores optaron por utilizar estas soluciones y reconocieron su eficacia para resolver sus problemas, se aceptaron como una forma estándar de resolver problemas y recibieron el nombre de «patrones de diseño»

A medida que se comprendía mejor la importancia de los patrones de diseño, éstos se fueron desarrollando y estandarizando. En la actualidad, la mayoría de los patrones de diseño modernos tienen una estructura definida, están organizados en varias categorías y se enseñan en las titulaciones relacionadas con la informática como temas independientes.

Tipos de Patrones de Diseño de JavaScript

Estas son algunas de las clasificaciones más populares de los patrones de diseño de JavaScript.

Creacionales

Los patrones de diseño creacionales son los que ayudan a resolver problemas relacionados con la creación y gestión de nuevas instancias de objetos en JavaScript. Puede ser tan sencillo como limitar una clase a tener un solo objeto o tan complejo como definir un intrincado método de selección y adición manual de cada característica en un objeto de JavaScript.

Algunos ejemplos de patrones de diseño de creación son Singleton, Factory, Abstract Factory y Builder, entre otros.

Estructurales

Los patrones de diseño estructurales son los que ayudan a resolver problemas relacionados con la gestión de la estructura (o esquema) de los objetos JavaScript. Estos problemas pueden incluir la creación de una relación entre dos objetos distintos o la abstracción de algunas características de un objeto para usuarios específicos.

Algunos ejemplos de patrones de diseño estructural son Adapter, Bridge, Composite, and Facade.

Conductuales

Los patrones de diseño conductuales son los que ayudan a resolver problemas sobre cómo se pasa el control (y la responsabilidad) entre varios objetos. Estos problemas podrían implicar controlar el acceso a una lista enlazada o establecer una única entidad que pueda controlar el acceso a varios tipos de objetos.

Algunos ejemplos de patrones de diseño conductuales son Command, Iterator, Memento, y Observer.

Concurrencia

Los patrones de diseño de concurrencia son los que ayudan a resolver problemas relacionados con el multihilo y la multitarea. Estos problemas pueden implicar el mantenimiento de un objeto activo entre múltiples objetos disponibles o el manejo de múltiples eventos suministrados a un sistema mediante la demultiplexación de la entrada entrante y su manejo pieza a pieza.

Algunos ejemplos de patrones de diseño de concurrencia son active object, nuclear react, y scheduler.

Arquitectónicos

Los patrones de diseño arquitectónico son los que ayudan a resolver problemas relacionados con el diseño de software en sentido amplio. Por lo general, están relacionados con la forma de diseñar tu sistema y garantizar una alta disponibilidad, mitigar los riesgos y evitar los cuellos de botella en el rendimiento.

Dos ejemplos de patrones de diseño arquitectónico son MVC y MVVM.

Elementos de un Patrón de Diseño

Casi todos los patrones de diseño pueden desglosarse en un conjunto de cuatro componentes importantes. Éstos son:

  • Nombre del patrón: Se utiliza para identificar un patrón de diseño mientras te comunicas con otros usuarios. Algunos ejemplos son «singleton», «prototype», etc.
  • Problema: Describe el objetivo del patrón de diseño. Es una pequeña descripción del problema que el patrón de diseño intenta resolver. Incluso puede incluir un escenario de ejemplo para explicar mejor el problema. También puede contener una lista de condiciones que deben cumplirse para que un patrón de diseño resuelva completamente el problema subyacente.
  • Solución: Es la solución al problema en cuestión, formada por elementos como clases, métodos, interfaces, etc. Es donde reside el grueso de un patrón de diseño — implica relaciones, responsabilidades y colaboradores de varios elementos que están claramente definidos.
  • Resultados: Se trata de un análisis de lo bien que el patrón fue capaz de resolver el problema. Se discuten aspectos como el uso de espacio y tiempo, junto con enfoques alternativos para resolver el mismo problema.

Si quieres saber más sobre los patrones de diseño y sus inicios, la MSU tiene un sucinto material de estudio que puedes consultar.

¿Por Qué Deberías Utilizar Patrones de Diseño?

Hay múltiples razones por las que deberías utilizar patrones de diseño:

  • Están probados y comprobados: Con un patrón de diseño, tienes una solución probada y comprobada para tu problema (siempre que el patrón de diseño se ajuste a la descripción de tu problema). No tienes que perder el tiempo buscando soluciones alternativas, y puedes estar seguro de que tienes una solución que se ocupa de la optimización básica del rendimiento por ti.
  • Son fáciles de entender: Los patrones de diseño están pensados para ser pequeños, sencillos y fáciles de entender. No necesitas ser un programador especializado que trabaje en un sector específico durante décadas para entender qué patrón de diseño utilizar. Son deliberadamente genéricos (no se limitan a ningún lenguaje de programación concreto) y puede entenderlos cualquiera que tenga suficientes habilidades para resolver problemas. Esto también ayuda cuando hay un cambio de manos en el equipo técnico: Un fragmento de código que se basa en un patrón de diseño es más fácil de entender para cualquier nuevo desarrollador de software.
  • Son sencillos de implementar: La mayoría de los patrones de diseño son muy sencillos, como verás más adelante en nuestro artículo. No necesitas conocer múltiples conceptos de programación para implementarlos en tu código.
  • Proponen una arquitectura del código fácilmente reutilizable: La reutilización y la limpieza del código se fomentan mucho en la industria tecnológica, y los patrones de diseño pueden ayudarte a conseguirlo. Puesto que estos patrones son una forma estándar de resolver problemas, sus diseñadores se han preocupado de garantizar que la arquitectura de la aplicación que los engloba siga siendo reutilizable, flexible y compatible con la mayoría de las formas de escribir código.
  • Ahorran tiempo y tamaño de la aplicación: Una de las mayores ventajas de confiar en un conjunto estándar de soluciones es que te ayudarán a ahorrar tiempo al implementarlas. Es muy probable que todo tu equipo de desarrollo conozca bien los patrones de diseño, por lo que les resultará más fácil planificar, comunicar y colaborar al implementarlos. Las soluciones probadas y comprobadas significan que hay muchas posibilidades de que no acabes perdiendo recursos o dando un rodeo mientras construyes alguna función, lo que te ahorrará tiempo y espacio. Además, la mayoría de los lenguajes de programación te proporcionan bibliotecas de plantillas estándar que ya implementan algunos patrones de diseño comunes, como Iterator y Observer.

Los 20 Mejores Patrones de Diseño de JavaScript que Debes Dominar

Ahora que ya sabes de qué está hecho un patrón de diseño y por qué los necesitas, vamos a profundizar en cómo se pueden implementar en una aplicación JavaScript algunos de los patrones de diseño de JavaScript más utilizados.

Creacionales

Empecemos el debate con algunos patrones de diseño creacionales fundamentales y fáciles de aprender.

1. Singleton

El patrón Singleton es uno de los patrones de diseño más utilizados en la industria del desarrollo de software. El problema que pretende resolver es mantener una única instancia de una clase. Esto puede resultar útil cuando se instancian objetos que consumen muchos recursos, como los manejadores de bases de datos.

A continuación te explicamos cómo puedes implementarlo en JavaScript:

function SingletonFoo() {

   let fooInstance = null;

   // For our reference, let's create a counter that will track the number of active instances
   let count = 0;

   function printCount() {
       console.log("Number of instances: " + count);
   }

   function init() {
       // For our reference, we'll increase the count by one whenever init() is called
       count++;

       // Do the initialization of the resource-intensive object here and return it
       return {}
   }

   function createInstance() {
       if (fooInstance == null) {
           fooInstance = init();
       }
       return fooInstance;
   }

   function closeInstance() {
       count--;
       fooInstance = null;
   }

   return {
       initialize: createInstance,
       close: closeInstance,
       printCount: printCount
   }
}

let foo = SingletonFoo();

foo.printCount() // Prints 0
foo.initialize()
foo.printCount() // Prints 1
foo.initialize()
foo.printCount() // Still prints 1
foo.initialize()
foo.printCount() // Still 1
foo.close()
foo.printCount() // Prints 0

Aunque cumple bien su propósito, el patrón Singleton es conocido porque dificulta la depuración, ya que enmascara las dependencias y controla el acceso a la inicialización o destrucción de las instancias de una clase.

2. Factory

El método Factory también es uno de los patrones de diseño más populares. El problema que pretende resolver el método Factory es crear objetos sin utilizar el constructor convencional. En su lugar, toma la configuración (o descripción) del objeto que deseas y devuelve el objeto recién creado.

Aquí te explicamos cómo puedes implementarlo en JavaScript:

function Factory() {
   this.createDog = function (breed) {
       let dog;

       if (breed === "labrador") {
           dog = new Labrador();
       } else if (breed === "bulldog") {
           dog = new Bulldog();
       } else if (breed === "golden retriever") {
           dog = new GoldenRetriever();
       } else if (breed === "german shepherd") {
           dog = new GermanShepherd();
       }

       dog.breed = breed;
       dog.printInfo = function () {
           console.log("nnBreed: " + dog.breed + "nShedding Level (out of 5): " + dog.sheddingLevel + "nCoat Length: " + dog.coatLength + "nCoat Type: " + dog.coatType)
       }

       return dog;
   }
}

function Labrador() {
   this.sheddingLevel = 4
   this.coatLength = "short"
   this.coatType = "double"
}

function Bulldog() {
   this.sheddingLevel = 3
   this.coatLength = "short"
   this.coatType = "smooth"
}

function GoldenRetriever() {
   this.sheddingLevel = 4
   this.coatLength = "medium"
   this.coatType = "double"
}

function GermanShepherd() {
   this.sheddingLevel = 4
   this.coatLength = "medium"
   this.coatType = "double"
}

function run() {

   let dogs = [];
   let factory = new Factory();

   dogs.push(factory.createDog("labrador"));
   dogs.push(factory.createDog("bulldog"));
   dogs.push(factory.createDog("golden retriever"));
   dogs.push(factory.createDog("german shepherd"));

   for (var i = 0, len = dogs.length; i < len; i++) {
       dogs[i].printInfo();
   }
}

run()

/**
Output:

Breed: labrador
Shedding Level (out of 5): 4
Coat Length: short
Coat Type: double


Breed: bulldog
Shedding Level (out of 5): 3
Coat Length: short
Coat Type: smooth


Breed: golden retriever
Shedding Level (out of 5): 4
Coat Length: medium
Coat Type: double


Breed: german shepherd
Shedding Level (out of 5): 4
Coat Length: medium
Coat Type: double
*/

El patrón de diseño Factory controla cómo se crearán los objetos y te proporciona una forma rápida de crear nuevos objetos, así como una interfaz uniforme que define las propiedades que tendrán tus objetos. Puedes añadir tantas razas de perros como quieras, siempre que los métodos y propiedades expuestos por los tipos de razas sigan siendo los mismos, funcionarán a la perfección.

Sin embargo, ten en cuenta que el patrón Factory a menudo puede dar lugar a un gran número de clases difíciles de gestionar.

3. Abstract Factory

El método Abstract Factory sube el nivel del método Factory al hacer que haya «factories» abstractas y, por tanto, sustituibles sin que el entorno de llamada conozca la factory exacta utilizada o su funcionamiento interno. El entorno de llamada sólo sabe que todas las «factories» tienen un conjunto de métodos comunes a los que puede llamar para realizar la acción de instanciación.

Así es como puede implementarse utilizando el ejemplo anterior:

// A factory to create dogs
function DogFactory() {
   // Notice that the create function is now createPet instead of createDog, since we need
   // it to be uniform across the other factories that will be used with this
   this.createPet = function (breed) {
       let dog;

       if (breed === "labrador") {
           dog = new Labrador();
       } else if (breed === "pug") {
           dog = new Pug();
       }

       dog.breed = breed;
       dog.printInfo = function () {
           console.log("nnType: " + dog.type + "nBreed: " + dog.breed + "nSize: " + dog.size)
       }

       return dog;
   }
}

// A factory to create cats
function CatFactory() {
   this.createPet = function (breed) {
       let cat;

       if (breed === "ragdoll") {
           cat = new Ragdoll();
       } else if (breed === "singapura") {
           cat = new Singapura();
       }

       cat.breed = breed;
       cat.printInfo = function () {
           console.log("nnType: " + cat.type + "nBreed: " + cat.breed + "nSize: " + cat.size)
       }

       return cat;
   }
}

// Dog and cat breed definitions
function Labrador() {
   this.type = "dog"
   this.size = "large"
}

function Pug() {
   this.type = "dog"
   this.size = "small"
}

function Ragdoll() {
   this.type = "cat"
   this.size = "large"
}

function Singapura() {
   this.type = "cat"
   this.size = "small"
}

function run() {

   let pets = [];

   // Initialize the two factories
   let catFactory = new CatFactory();
   let dogFactory = new DogFactory();

   // Create a common petFactory that can produce both cats and dogs
   // Set it to produce dogs first
   let petFactory = dogFactory;

   pets.push(petFactory.createPet("labrador"));
   pets.push(petFactory.createPet("pug"));

   // Set the petFactory to produce cats
   petFactory = catFactory;

   pets.push(petFactory.createPet("ragdoll"));
   pets.push(petFactory.createPet("singapura"));

   for (var i = 0, len = pets.length; i < len; i++) {
       pets[i].printInfo();
   }
}

run()

/**
Output:

Type: dog
Breed: labrador
Size: large


Type: dog
Breed: pug
Size: small


Type: cat
Breed: ragdoll
Size: large


Type: cat
Breed: singapura
Size: small

*/

El patrón Abstract Factory te facilita el intercambio de factories concretas, y ayuda a promover la uniformidad entre las factories y los productos creados. Sin embargo, puede resultar difícil introducir nuevos tipos de productos, ya que tendrías que hacer cambios en varias clases para dar cabida a nuevos métodos/propiedades.

4. Builder

El patrón Builder es uno de los patrones de diseño creacionales de JavaScript más complejos y flexibles. Te permite construir una a una las características de tu producto, proporcionándote un control total sobre cómo se construye tu objeto, al tiempo que abstrae los detalles internos.

En el intrincado ejemplo de abajo, verás el patrón de diseño Builder en acción con Director para ayudar a hacer ¡Pizzas!

// Here's the PizzaBuilder (you can also call it the chef)
function PizzaBuilder() {
   let base
   let sauce
   let cheese
   let toppings = []

   // The definition of pizza is hidden from the customers
   function Pizza(base, sauce, cheese, toppings) {
       this.base = base
       this.sauce = sauce
       this.cheese = cheese
       this.toppings = toppings

       this.printInfo = function() {
           console.log("This pizza has " + this.base + " base with " + this.sauce + " sauce "
           + (this.cheese !== undefined ? "with cheese. " : "without cheese. ")
           + (this.toppings.length !== 0 ? "It has the following toppings: " + toppings.toString() : ""))
       }
   }

   // You can request the PizzaBuilder (/chef) to perform any of the following actions on your pizza
   return {
       addFlatbreadBase: function() {
           base = "flatbread"
           return this;
       },
       addTomatoSauce: function() {
           sauce = "tomato"
           return this;
       },
       addAlfredoSauce: function() {
           sauce = "alfredo"
           return this;
       },
       addCheese: function() {
           cheese = "parmesan"
           return this;
       },
       addOlives: function() {
           toppings.push("olives")
           return this
       },
       addJalapeno: function() {
           toppings.push("jalapeno")
           return this
       },
       cook: function() {
           if (base === null){
               console.log("Can't make a pizza without a base")
               return
           }
           return new Pizza(base, sauce, cheese, toppings)
       }
   }

}

// This is the Director for the PizzaBuilder, aka the PizzaShop.
// It contains a list of preset steps that can be used to prepare common pizzas (aka recipes!)
function PizzaShop() {
   return {
       makePizzaMargherita: function() {
           pizzaBuilder = new PizzaBuilder()
           pizzaMargherita = pizzaBuilder.addFlatbreadBase().addTomatoSauce().addCheese().addOlives().cook()
           return pizzaMargherita
       },
       makePizzaAlfredo: function() {
           pizzaBuilder = new PizzaBuilder()
           pizzaAlfredo = pizzaBuilder.addFlatbreadBase().addAlfredoSauce().addCheese().addJalapeno().cook()
           return pizzaAlfredo
       },
       makePizzaMarinara: function() {
           pizzaBuilder = new PizzaBuilder()
           pizzaMarinara = pizzaBuilder.addFlatbreadBase().addTomatoSauce().addOlives().cook()
           return pizzaMarinara
       }
   }
}

// Here's where the customer can request pizzas from
function run() {

   let pizzaShop = new PizzaShop()

   // You can ask for one of the popular pizza recipes...
   let pizzaMargherita = pizzaShop.makePizzaMargherita()
   pizzaMargherita.printInfo()
   // Output: This pizza has flatbread base with tomato sauce with cheese. It has the following toppings: olives

   let pizzaAlfredo = pizzaShop.makePizzaAlfredo()
   pizzaAlfredo.printInfo()
   // Output: This pizza has flatbread base with alfredo sauce with cheese. It has the following toppings: jalapeno

   let pizzaMarinara = pizzaShop.makePizzaMarinara()
   pizzaMarinara.printInfo()
   // Output: This pizza has flatbread base with tomato sauce without cheese. It has the following toppings: olives

   // Or send your custom request directly to the chef!
   let chef = PizzaBuilder()
   let customPizza = chef.addFlatbreadBase().addTomatoSauce().addCheese().addOlives().addJalapeno().cook()
   customPizza.printInfo()
   // Output: This pizza has flatbread base with tomato sauce with cheese. It has the following toppings: olives,jalapeno

}

run()

Puede conectar el Builder con un Director, , como muestra la clase PizzaShop en el ejemplo anterior, para predefinir un conjunto de pasos a seguir cada vez que construyas una variante estándar de tu producto, es decir, una receta específica para tus pizzas.

El único problema de este patrón de diseño es que es bastante complejo de configurar y mantener. Sin embargo, añadir nuevas funciones de esta forma es más sencillo que con el método Factory.

5. Prototype

El patrón de diseño Prototype es una forma rápida y sencilla de crear nuevos objetos a partir de objetos existentes, clonándolos.

Primero se crea un objeto prototipo, que puede clonarse varias veces para crear nuevos objetos. Resulta útil cuando instanciar directamente un objeto es una operación que consume más recursos que crear una copia de uno existente.

En el siguiente ejemplo, verás cómo puedes utilizar el patrón Prototype para crear nuevos documentos basados en un documento plantilla establecido:

// Defining how a document would look like
function Document() {
   this.header = "Acme Co"
   this.footer = "For internal use only"
   this.pages = 2
   this.text = ""
  
   this.addText = function(text) {
       this.text += text
   }

   // Method to help you see the contents of the object
   this.printInfo = function() {
       console.log("nnHeader: " + this.header + "nFooter: " + this.footer + "nPages: " + this.pages + "nText: " + this.text)
   }

  
}

// A protype (or template) for creating new blank documents with boilerplate information
function DocumentPrototype(baseDocument) {
   this.baseDocument = baseDocument
  
   // This is where the magic happens. A new document object is created and is assigned the values of the current object
   this.clone = function() {
       let document = new Document();

       document.header = this.baseDocument.header
       document.footer = this.baseDocument.footer
       document.pages = this.baseDocument.pages
       document.text = this.baseDocument.text

       return document
   }
}

function run() {
   // Create a document to use as the base for the prototype
   let baseDocument = new Document()

   // Make some changes to the prototype
   baseDocument.addText("This text was added before cloning and will be common in both documents. ")

   let prototype = new DocumentPrototype(baseDocument)

   // Create two documents from the prototype
   let doc1 = prototype.clone()
   let doc2 = prototype.clone()

   // Make some changes to both objects
   doc1.pages = 3

   doc1.addText("This is document 1")
   doc2.addText("This is document 2")

   // Print their values
   doc1.printInfo()
   /* Output:
       Header: Acme Co
       Footer: For internal use only
       Pages: 3
       Text: This text was added before cloning and will be common in both documents. This is document 1
    */

   doc2.printInfo()
   /** Output:
       Header: Acme Co
       Footer: For internal use only
       Pages: 2
       Text: This text was added before cloning and will be common in both documents. This is document 2
    */
}

run()

El método Prototype funciona muy bien para los casos en que una gran parte de tus objetos comparten los mismos valores, o cuando crear un objeto nuevo por completo es bastante costoso. Sin embargo, parece una exageración en casos en los que no necesitas más que unas pocas instancias de la clase.

Estructurales

Los patrones de diseño estructurales te ayudan a organizar tu lógica de negocio proporcionándote formas probadas de estructurar tus clases. Existe una gran variedad de patrones de diseño estructural que se adaptan a casos de uso únicos.

6. Adapter

Un problema habitual al crear aplicaciones es permitir la colaboración entre clases incompatibles.

Un buen ejemplo para entender esto es mantener la compatibilidad hacia atrás. Si escribes una nueva versión de una clase, naturalmente querrás que sea fácilmente utilizable en todos los lugares donde funcionaba la versión antigua. Sin embargo, si haces cambios de ruptura, como eliminar o actualizar métodos que eran cruciales para el funcionamiento de la versión antigua, puedes acabar con una clase que necesite que se actualicen todos sus clientes para poder ejecutarse.

En estos casos, el patrón de diseño Adapter puede ser de ayuda.

El patrón de diseño Adapter te proporciona una abstracción que sirve de puente entre los métodos y propiedades de la nueva clase y los métodos y propiedades de la antigua. Tiene la misma interfaz que la clase antigua, pero contiene lógica para asignar los métodos antiguos a los nuevos para ejecutar operaciones similares. Esto es similar a cómo una toma de corriente actúa como adaptador entre un enchufe de estilo estadounidense y un enchufe de estilo europeo.

Aquí tienes un ejemplo:

// Old bot
function Robot() {

   this.walk = function(numberOfSteps) {
       // code to make the robot walk
       console.log("walked " + numberOfSteps + " steps")
   }

   this.sit = function() {
       // code to make the robot sit
       console.log("sit")
   }

}

// New bot that does not have the walk function anymore
// but instead has functions to control each step independently
function AdvancedRobot(botName) {
   // the new bot has a name as well
   this.name = botName

   this.sit = function() {
       // code to make the robot sit
       console.log("sit")
   }

   this.rightStepForward = function() {
       // code to take 1 step from right leg forward
       console.log("right step forward")
   }

   this.leftStepForward = function () {
       // code to take 1 step from left leg forward
       console.log("left step forward")
   }
}

function RobotAdapter(botName) {
   // No references to the old interfact since that is usually
   // phased out of development
   const robot = new AdvancedRobot(botName)

   // The adapter defines the walk function by using the
   // two step controls. You now have room to choose which leg to begin/end with,
   // and do something at each step.
   this.walk = function(numberOfSteps) {
       for (let i=0; i<numberOfSteps; i++) {
          
           if (i % 2 === 0) {
               robot.rightStepForward()
           } else {
               robot.leftStepForward()
           }
       }
   }

   this.sit = robot.sit

}

function run() {

   let robot = new Robot()

   robot.sit()
   // Output: sit
   robot.walk(5)
   // Output: walked 5 steps

   robot = new RobotAdapter("my bot")

   robot.sit()
   // Output: sit
   robot.walk(5)
   // Output:
   // right step forward
   // left step forward
   // right step forward
   // left step forward
   // right step forward

}

run()

El principal problema de este patrón de diseño es que añade complejidad a tu código fuente. Ya tenías que mantener dos clases diferentes, y ahora tienes otra clase  — la Adapter — que mantener.

7. Bridge

Ampliando el patrón Adapter, el patrón de diseño Bridge proporciona tanto a la clase como al cliente interfaces independientes para que ambos puedan funcionar incluso en casos de interfaces nativas incompatibles.

Ayuda a desarrollar una interfaz muy poco acoplada entre los dos tipos de objetos. También ayuda a mejorar la extensibilidad de las interfaces y sus implementaciones para obtener la máxima flexibilidad.

A continuación te explicamos cómo puedes utilizarlo:

// The TV and speaker share the same interface
function TV() {
   this.increaseVolume = function() {
       // logic to increase TV volume
   }

   this.decreaseVolume = function() {
       // logic to decrease TV volume
   }

   this.mute = function() {
       // logic to mute TV audio
   }
}

function Speaker() {
   this.increaseVolume = function() {
       // logic to increase speaker volume
   }

   this.decreaseVolume = function() {
       // logic to decrease speaker volume
   }

   this.mute() = function() {
       // logic to mute speaker audio
   }
}

// The two remotes make use of the same common interface
// that supports volume up and volume down features
function SimpleRemote(device) {
   this.pressVolumeDownKey = function() {
       device.decreaseVolume()
   }

   this.pressVolumeUpKey = function() {
       device.increaseVolume()
   }
}

function AdvancedRemote(device) {

   this.pressVolumeDownKey = function() {
       device.decreaseVolume()
   }

   this.pressVolumeUpKey = function() {
       device.increaseVolume()
   }

   this.pressMuteKey = function() {
       device.mute()
   }
}

function run() {

   let tv = new TV()
   let speaker = new Speaker()

   let tvSimpleRemote = new SimpleRemote(tv)
   let tvAdvancedRemote = new AdvancedRemote(tv)

   let speakerSimpleRemote = new SimpleRemote(speaker)
   let speakerAdvancedRemote = new AdvancedRemote(speaker)

   // The methods listed in pair below will have the same effect
   // on their target devices
   tvSimpleRemote.pressVolumeDownKey()
   tvAdvancedRemote.pressVolumeDownKey()

   tvSimpleRemote.pressVolumeUpKey()
   tvAdvancedRemote.pressVolumeUpKey()

   // The advanced remote has additional functionality
   tvAdvancedRemote.pressMuteKey()

   speakerSimpleRemote.pressVolumeDownKey()
   speakerAdvancedRemote.pressVolumeDownKey()

   speakerSimpleRemote.pressVolumeUpKey()
   speakerAdvancedRemote.pressVolumeUpKey()

   speakerAdvancedRemote.pressMuteKey()
}

Como ya habrás adivinado, el patrón Bridge aumenta enormemente la complejidad de la base de código. Además, la mayoría de las interfaces suelen acabar con una sola implementación en los casos de uso del mundo real, por lo que realmente no te beneficias mucho de la reutilización del código.

8. Composite

El patrón de diseño Composite te ayuda a estructurar y gestionar fácilmente objetos y entidades similares. La idea básica del patrón Composite es que los objetos y sus contenedores lógicos pueden representarse utilizando una única clase abstracta (que puede almacenar datos/métodos relacionados con el objeto y referencias a sí misma para el contenedor).

Tiene más sentido utilizar el patrón Composite cuando tu modelo de datos se asemeja a una estructura de árbol. Sin embargo, no debes intentar convertir un modelo de datos no arbóreo en un modelo de datos arbóreo sólo por utilizar el patrón Composite, ya que hacerlo a menudo puede restarle mucha flexibilidad.

En el siguiente ejemplo, verás cómo puedes utilizar el patrón de diseño Composite para construir un sistema de empaquetado para productos de comercio electrónico que también pueda calcular el valor total del pedido por paquete:

// A product class, that acts as a Leaf node
function Product(name, price) {
   this.name = name
   this.price = price

   this.getTotalPrice = function() {
       return this.price
   }
}

// A box class, that acts as a parent/child node
function Box(name) {
   this.contents = []
   this.name = name

   // Helper function to add an item to the box
   this.add = function(content){
       this.contents.push(content)
   }

   // Helper function to remove an item from the box
   this.remove = function() {
       var length = this.contents.length;
       for (var i = 0; i < length; i++) {
           if (this.contents[i] === child) {
               this.contents.splice(i, 1);
               return;
           }
       }
   }

   // Helper function to get one item from the box
   this.getContent = function(position) {
       return this.contents[position]
   }

   // Helper function to get the total count of the items in the box
   this.getTotalCount = function() {
       return this.contents.length
   }

   // Helper function to calculate the total price of all items in the box
   this.getTotalPrice = function() {
       let totalPrice = 0;

       for (let i=0; i < this.getTotalCount(); i++){
           totalPrice += this.getContent(i).getTotalPrice()
       }

       return totalPrice
   }
}

function run() {

   // Let's create some electronics
   const mobilePhone = new Product("mobile phone," 1000)
   const phoneCase = new Product("phone case," 30)
   const screenProtector = new Product("screen protector," 20)

   // and some stationery products
   const pen = new Product("pen," 2)
   const pencil = new Product("pencil," 0.5)
   const eraser = new Product("eraser," 0.5)
   const stickyNotes = new Product("sticky notes," 10)

   // and put them in separate boxes
   const electronicsBox = new Box("electronics")
   electronicsBox.add(mobilePhone)
   electronicsBox.add(phoneCase)
   electronicsBox.add(screenProtector)
  
   const stationeryBox = new Box("stationery")
   stationeryBox.add(pen)
   stationeryBox.add(pencil)
   stationeryBox.add(eraser)
   stationeryBox.add(stickyNotes)

   // and finally, put them into one big box for convenient shipping
   const package = new Box('package')
   package.add(electronicsBox)
   package.add(stationeryBox)

   // Here's an easy way to calculate the total order value
   console.log("Total order price: USD " + package.getTotalPrice())
   // Output: USD 1063
}

run()

El mayor inconveniente de utilizar el patrón Composite es que los cambios en las interfaces de los componentes pueden ser muy complicados en el futuro. Diseñar las interfaces requiere tiempo y esfuerzo, y la naturaleza arborescente del modelo de datos puede dificultar mucho la introducción de cambios a tu antojo.

9. Decorator

El patrón Decorator te ayuda a añadir nuevas funciones a los objetos existentes simplemente envolviéndolos dentro de un nuevo objeto. Es similar a cómo puedes envolver una caja de regalo ya envuelta con papel de regalo nuevo tantas veces como quieras: Cada envoltorio te permite añadir tantas funciones como quieras, así que es muy flexible.

Desde un punto de vista técnico, no hay herencia, por lo que hay mayor libertad a la hora de diseñar la lógica empresarial.

En el ejemplo siguiente, verás cómo el patrón Decorator ayuda a añadir más funciones a una clase estándar de Customer:

function Customer(name, age) {
   this.name = name
   this.age = age

   this.printInfo = function() {
       console.log("Customer:nName : " + this.name + " | Age: " + this.age)
   }
}

function DecoratedCustomer(customer, location) {
   this.customer = customer
   this.name = customer.name
   this.age = customer.age
   this.location = location

   this.printInfo = function() {
       console.log("Decorated Customer:nName: " + this.name + " | Age: " + this.age + " | Location: " + this.location)
   }
}

function run() {
   let customer = new Customer("John," 25)
   customer.printInfo()
   // Output:
   // Customer:
   // Name : John | Age: 25

   let decoratedCustomer = new DecoratedCustomer(customer, "FL")
   decoratedCustomer.printInfo()
   // Output:
   // Customer:
   // Name : John | Age: 25 | Location: FL
}

run()

Las desventajas de este patrón incluyen la alta complejidad del código, ya que no existe un patrón estándar definido para añadir nuevas funciones mediante decoradores. Podrías acabar con un montón de decoradores no uniformes y/o similares al final de tu ciclo de vida de desarrollo de software.

Si no tienes cuidado al diseñar los decoradores, puedes acabar diseñando unos decoradores que dependan lógicamente de otros. Si esto no se resuelve, eliminar o reestructurar los decoradores más adelante puede causar estragos en la estabilidad de tu aplicación.

10. Facade

Al construir la mayoría de las aplicaciones del mundo real, la lógica de negocio suele resultar bastante compleja cuando terminas. Puede que acabes con múltiples objetos y métodos implicados en la ejecución de las operaciones principales de tu aplicación. Hacer un seguimiento de sus inicializaciones, dependencias, el orden correcto de ejecución de los métodos, etc., puede ser bastante complicado y propenso a errores si no se hace correctamente.

El patrón de diseño Facade te ayuda a crear una abstracción entre el entorno que invoca las operaciones mencionadas y los objetos y métodos implicados en la realización de esas operaciones. Esta abstracción alberga la lógica para inicializar los objetos, rastrear sus dependencias y otras actividades importantes. El entorno de llamada no tiene información sobre cómo se ejecuta una operación. Puedes actualizar libremente la lógica sin realizar ningún cambio de ruptura en el cliente llamante.

A continuación te explicamos cómo utilizarlo en una aplicación:

/**
* Let's say you're trying to build an online store. It will have multiple components and
* complex business logic. In the example below, you will find a tiny segment of an online
* store composed together using the Facade design pattern. The various manager and helper
* classes are defined first of all.
*/


function CartManager() {
   this.getItems = function() {
       // logic to return items
       return []
   }
  
   this.clearCart = function() {
       // logic to clear cart
   }
}

function InvoiceManager() {
   this.createInvoice = function(items) {
       // logic to create invoice
       return {}
   }

   this.notifyCustomerOfFailure = function(invoice) {
       // logic to notify customer
   }

   this.updateInvoicePaymentDetails = function(paymentResult) {
       // logic to update invoice after payment attempt
   }
}

function PaymentProcessor() {
   this.processPayment = function(invoice) {
       // logic to initiate and process payment
       return {}
   }
}

function WarehouseManager() {
   this.prepareForShipping = function(items, invoice) {
       // logic to prepare the items to be shipped
   }
}

// This is where facade comes in. You create an additional interface on top of your
// existing interfaces to define the business logic clearly. This interface exposes
// very simple, high-level methods for the calling environment.
function OnlineStore() {
   this.name = "Online Store"
  
   this.placeOrder = function() {
       let cartManager = new CartManager()
       let items = cartManager.getItems()

       let invoiceManager = new InvoiceManager()
       let invoice = invoiceManager.createInvoice(items)
      
       let paymentResult = new PaymentProcessor().processPayment(invoice)
       invoiceManager.updateInvoicePaymentDetails(paymentResult)

       if (paymentResult.status === 'success') {
           new WarehouseManager().prepareForShipping(items, invoice)
           cartManager.clearCart()
       } else {
           invoiceManager.notifyCustomerOfFailure(invoice)
       }
      
   }
}

// The calling environment is unaware of what goes on when somebody clicks a button to
// place the order. You can easily change the underlying business logic without breaking
// your calling environment.
function run() {
   let onlineStore = new OnlineStore()

   onlineStore.placeOrder()
}

Una desventaja de utilizar el patrón Facade es que añade una capa adicional de abstracción entre tu lógica de negocio y el cliente, lo que requiere un mantenimiento adicional. En la mayoría de los casos, esto aumenta la complejidad general del código base.

Además, la clase Facade se convierte en una dependencia obligatoria para el funcionamiento de tu aplicación, lo que significa que cualquier error en la clase Facade repercute directamente en el funcionamiento de tu aplicación.

11. Flyweight

El patrón Flyweight te ayuda a resolver problemas que implican objetos con componentes que se repiten de forma eficiente en memoria, ayudándote a reutilizar los componentes comunes de tu conjunto de objetos. Esto ayuda a reducir la carga en la memoria y también da lugar a tiempos de ejecución más rápidos.

En el siguiente ejemplo, se almacena una frase larga en la memoria utilizando el patrón de diseño Flyweight. En lugar de almacenar cada carácter a medida que se produce, el programa identifica el conjunto de caracteres distintos que se han utilizado para escribir el párrafo y sus tipos (número o alfabeto) y construye flyweights reutilizables para cada carácter que contienen detalles sobre qué carácter y tipo se almacenan.

Después, la matriz principal sólo almacena una lista de referencias a estos pesos volantes en el orden en que aparecen en la frase, en lugar de almacenar una instancia del objeto carácter cada vez que aparece.

Esto reduce a la mitad la memoria que ocupa la frase. Ten en cuenta que ésta es una explicación muy básica de cómo almacenan el texto los procesadores de texto.

// A simple Character class that stores the value, type, and position of a character
function Character(value, type, position) {
   this.value = value
   this.type = type
   this.position = position
}

// A Flyweight class that stores character value and type combinations
function CharacterFlyweight(value, type) {
   this.value = value
   this.type = type
}

// A factory to automatically create the flyweights that are not present in the list,
// and also generate a count of the total flyweights in the list
const CharacterFlyweightFactory = (function () {
   const flyweights = {}

   return {
       get: function (value, type) {
           if (flyweights[value + type] === undefined)
               flyweights[value + type] = new CharacterFlyweight(value, type)

           return flyweights[value + type]
       },
       count: function () {
           let count = 0;
           for (var f in flyweights) count++;
           return count;
       }
   }
})()

// An enhanced Character class that uses flyweights to store references
// to recurring value and type combinations
function CharacterWithFlyweight(value, type, position) {
   this.flyweight = CharacterFlyweightFactory.get(value, type)
   this.position = position
}

// A helper function to define the type of a character
// It identifies numbers as N and everything as A (for alphabets)
function getCharacterType(char) {
   switch (char) {
       case "0":
       case "1":
       case "2":
       case "3":
       case "4":
       case "5":
       case "6":
       case "7":
       case "8":
       case "9": return "N"
       default:
           return "A"

   }
}

// A list class to create an array of Characters from a given string
function CharactersList(str) {
   chars = []
   for (let i = 0; i < str.length; i++) {
       const char = str[i]
       chars.push(new Character(char, getCharacterType(char), i))
   }

   return chars
}

// A list class to create an array of CharacterWithFlyweights from a given string
function CharactersWithFlyweightsList(str) {
   chars = []
   for (let i = 0; i  " + charactersList.length)
   // Output: Character count -> 656

   // The number of flyweights created is only 31, since only 31 characters are used to write the
   // entire paragraph. This means that to store 656 characters, a total of
   // (31 * 2 + 656 * 1 = 718) memory blocks are used instead of (656 * 3 = 1968) which would have
   // used by the standard array.
   // (We have assumed each variable to take up one memory block for simplicity. This
   // may vary in real-life scenarios)
   console.log("Flyweights created -> " + CharacterFlyweightFactory.count())
   // Output: Flyweights created -> 31

}

run()

Como ya te habrás dado cuenta, el patrón Flyweight añade complejidad al diseño de tu software al no ser especialmente intuitivo. Así que, si ahorrar memoria no es una preocupación acuciante para tu aplicación, la complejidad añadida de Flyweight puede hacer más mal que bien.

Además, los Flyweight intercambian memoria por eficiencia de procesamiento, así que si te faltan ciclos de CPU, Flyweight no es una buena solución para ti.

12. Proxy

El patrón Proxy te ayuda a sustituir un objeto por otro. En otras palabras, los objetos proxy pueden ocupar el lugar de objetos reales (de los que son un proxy) y controlar el acceso al objeto. Estos objetos proxy pueden utilizarse para realizar algunas acciones antes o después de pasar una solicitud de invocación al objeto real.

En el ejemplo siguiente, verás cómo se controla el acceso a una instancia de base de datos mediante un proxy que realiza algunas comprobaciones básicas de validación de las solicitudes antes de permitir su paso:

function DatabaseHandler() {
   const data = {}

   this.set = function (key, val) {
       data[key] = val;
   }
   this.get = function (key, val) {
       return data[key]
   }
   this.remove = function (key) {
       data[key] = null;
   }


}

function DatabaseProxy(databaseInstance) {

   this.set = function (key, val) {
       if (key === "") {
           console.log("Invalid input")
           return
       }

       if (val === undefined) {
           console.log("Setting value to undefined not allowed!")
           return
       }

       databaseInstance.set(key, val)
   }

   this.get = function (key) {
       if (databaseInstance.get(key) === null) {
           console.log("Element deleted")
       }

       if (databaseInstance.get(key) === undefined) {
           console.log("Element not created")
       }

       return databaseInstance.get(key)
   }

   this.remove = function (key) {
       if (databaseInstance.get(key) === undefined) {
           console.log("Element not added")
           return
       }

       if (databaseInstance.get(key) === null) {
           console.log("Element removed already")
           return
       }

       return databaseInstance.remove(key)
   }

}

function run() {
   let databaseInstance = new DatabaseHandler()

   databaseInstance.set("foo," "bar")
   databaseInstance.set("foo," undefined)
   console.log("#1: " + databaseInstance.get("foo"))
   // #1: undefined

   console.log("#2: " + databaseInstance.get("baz"))
   // #2: undefined

   databaseInstance.set("," "something")

   databaseInstance.remove("foo")
   console.log("#3: " + databaseInstance.get("foo"))
   // #3: null

   databaseInstance.remove("foo")
   databaseInstance.remove("baz")

   // Create a fresh database instance to try the same operations
   // using the proxy
   databaseInstance = new DatabaseHandler()
   let proxy = new DatabaseProxy(databaseInstance)

   proxy.set("foo," "bar")
   proxy.set("foo," undefined)
   // Proxy jumps in:
   // Output: Setting value to undefined not allowed!

   console.log("#1: " + proxy.get("foo"))
   // Original value is retained:
   // Output: #1: bar

   console.log("#2: " + proxy.get("baz"))
   // Proxy jumps in again
   // Output:
   // Element not created
   // #2: undefined


   proxy.set("," "something")
   // Proxy jumps in again
   // Output: Invalid input

   proxy.remove("foo")

   console.log("#3: " + proxy.get("foo"))
   // Proxy jumps in again
   // Output:
   // Element deleted
   // #3: null

   proxy.remove("foo")
   // Proxy output: Element removed already
   proxy.remove("baz")
   // Proxy output: Element not added

}

run()

Este patrón de diseño se utiliza habitualmente en todo el sector y ayuda a implementar fácilmente operaciones previas y posteriores a la ejecución. Sin embargo, como cualquier otro patrón de diseño, también añade complejidad a tu código base, así que intenta no utilizarlo si no lo necesitas realmente.

También deberás tener en cuenta que, dado que interviene un objeto adicional al realizar llamadas a tu objeto principal, puede haber cierta latencia debida a las operaciones de procesamiento añadidas. Optimizar el rendimiento de tu objeto principal ahora también implica optimizar el rendimiento de los métodos de tu proxy.

Conductuales

Los patrones de diseño conductuales te ayudan a resolver problemas sobre cómo interactúan los objetos entre sí. Esto puede implicar compartir o pasar responsabilidad/control entre objetos para completar operaciones de conjunto. También puede implicar pasar/compartir datos entre varios objetos de la forma más eficiente posible.

13. Chain of Responsibility

El patrón Chain of Responsibility es uno de los patrones de diseño de comportamiento más sencillos. Resulta útil cuando diseñas la lógica de operaciones que pueden ser gestionadas por varios controladores.

De forma similar a cómo funciona el escalado de incidencias en los equipos de soporte, el control pasa a través de una cadena de manejadores, y el manejador responsable de realizar la acción completa la operación. Este patrón de diseño se utiliza a menudo en el diseño de IU, donde varias capas de componentes pueden gestionar un evento de entrada del usuario, como un toque o un deslizamiento.

A continuación verás un ejemplo de escalado de una queja utilizando el patrón Chain of Responsibility. La reclamación será tratada por los gestores en función de su gravedad:

// Complaint class that stores title and severity of a complaint
// Higher value of severity indicates a more severe complaint
function Complaint (title, severity) {
    this.title = title
    this.severity = severity
}

// Base level handler that receives all complaints
function Representative () {
    // If this handler can not handle the complaint, it will be forwarded to the next level
    this.nextLevel = new Management()

    this.handleComplaint = function (complaint) {
        if (complaint.severity === 0)
            console.log("Representative resolved the following complaint: " + complaint.title)
        else
            this.nextLevel.handleComplaint(complaint)
    }
}

// Second level handler to handle complaints of severity 1
function Management() {
    // If this handler can not handle the complaint, it will be forwarded to the next level
    this.nextLevel = new Leadership()

    this.handleComplaint = function (complaint) {
        if (complaint.severity === 1)
            console.log("Management resolved the following complaint: " + complaint.title)
        else
            this.nextLevel.handleComplaint(complaint)
    }
}

// Highest level handler that handles all complaints unhandled so far
function Leadership() {
    this.handleComplaint = function (complaint) {
        console.log("Leadership resolved the following complaint: " + complaint.title)
    }
}

function run() {
    // Create an instance of the base level handler
    let customerSupport = new Representative()

    // Create multiple complaints of varying severity and pass them to the base handler

    let complaint1 = new Complaint("Submit button doesn't work," 0)
    customerSupport.handleComplaint(complaint1)
    // Output: Representative resolved the following complaint: Submit button doesn't work

    let complaint2 = new Complaint("Payment failed," 1)
    customerSupport.handleComplaint(complaint2)
    // Output: Management resolved the following complaint: Payment failed

    let complaint3 = new Complaint("Employee misdemeanour," 2)
    customerSupport.handleComplaint(complaint3)
    // Output: Leadership resolved the following complaint: Employee misdemeanour
}

run()

El problema obvio de este diseño es que es lineal, por lo que puede haber cierta latencia en la gestión de una operación cuando un gran número de gestores están encadenados entre sí.

Hacer un seguimiento de todos los manipuladores puede ser otro punto doloroso, ya que puede volverse bastante desordenado a partir de un cierto número de manipuladores. La depuración es otra pesadilla, ya que cada solicitud puede terminar en un manejador diferente, lo que te dificulta estandarizar el proceso de registro y depuración.

14. Iterator

El patrón Iterator es bastante sencillo y se utiliza con mucha frecuencia en casi todos los lenguajes modernos orientados a objetos. Si te encuentras ante la tarea de recorrer una lista de objetos que no son todos del mismo tipo, los métodos de iteración normales, como un bucle for, pueden resultar bastante complicados — sobre todo si también estás escribiendo lógica de negocio dentro de ellos.

El patrón Iterator puede ayudarte a aislar la lógica de iteración y procesamiento de tus listas de la lógica de negocio principal.

A continuación te mostramos cómo puedes utilizarlo en una lista bastante básica con varios tipos de elementos:

// Iterator for a complex list with custom methods
function Iterator(list) {
   this.list = list
   this.index = 0

   // Fetch the current element
   this.current = function() {
       return this.list[this.index]
   }

   // Fetch the next element in the list
   this.next = function() {
       return this.list[this.index++]
   }

   // Check if there is another element in the list
   this.hasNext = function() {
       return this.index < this.list.length
   }

   // Reset the index to point to the initial element
   this.resetIndex = function() {
       this.index = 0
   }

   // Run a forEach loop over the list
   this.forEach = function(callback) {
       for (let element = this.next(); this.index <= this.list.length; element = this.next()) {
           callback(element)
       }
   }
}

function run() {
   // A complex list with elements of multiple data types
   let list = ["Lorem ipsum," 9, ["lorem ipsum dolor," true], false]

   // Create an instance of the iterator and pass it the list
   let iterator = new Iterator(list)

   // Log the first element
   console.log(iterator.current())
   // Output: Lorem ipsum

   // Print all elements of the list using the iterator's methods
   while (iterator.hasNext()) {
       console.log(iterator.next())
       /**
        * Output:
        * Lorem ipsum
        * 9
        * [ 'lorem ipsum dolor', true ]
        * false
        */
   }

   // Reset the iterator's index to the first element
   iterator.resetIndex()

   // Use the custom iterator to pass an effect that will run for each element of the list
   iterator.forEach(function (element) {
       console.log(element)
   })
   /**
    * Output:
    * Lorem ipsum
    * 9
    * [ 'lorem ipsum dolor', true ]
    * false
    */
}

run()

No hace falta decir que este patrón puede ser innecesariamente complejo para listas sin múltiples tipos de elementos. Además, si hay demasiados tipos de elementos en una lista, también puede resultar difícil de gestionar.

La clave está en identificar si realmente necesitas un iterador en función de tu lista y de sus posibilidades de cambio en el futuro. Además, el patrón Iterator sólo es útil en listas, y las listas a veces pueden limitarte a su modo lineal de acceso. Otras estructuras de datos pueden proporcionarte a veces mayores ventajas de rendimiento.

15. Mediator

El diseño de tu aplicación a veces puede requerir que juegues con un gran número de objetos distintos que albergan diversos tipos de lógica empresarial y que a menudo dependen unos de otros. Manejar las dependencias a veces puede resultar complicado, ya que necesitas hacer un seguimiento de cómo estos objetos intercambian datos y control entre ellos.

El patrón de diseño Mediator pretende ayudarte a resolver este problema aislando la lógica de interacción de estos objetos en un objeto independiente por sí mismo.

Este objeto separado se conoce como mediador, y es el responsable de que tus clases de nivel inferior realicen el trabajo. Tu cliente o el entorno de llamada también interactuarán con el mediador en lugar de con las clases de nivel inferior.

Aquí tienes un ejemplo del patrón de diseño mediator en acción:

// Writer class that receives an assignment, writes it in 2 seconds, and marks it as finished
function Writer(name, manager) {
    
    // Reference to the manager, writer's name, and a busy flag that the manager uses while assigning the article
    this.manager = manager
    this.name = name
    this.busy = false

    this.startWriting = function (assignment) {
        console.log(this.name + " started writing "" + assignment + """)
        this.assignment = assignment
        this.busy = true

        // 2 s timer to replicate manual action
        setTimeout(() => { this.finishWriting() }, 2000)
    }

    this.finishWriting = function () {
        if (this.busy === true) {
            console.log(this.name + " finished writing "" + this.assignment + """)
            this.busy = false
            return this.manager.notifyWritingComplete(this.assignment)
        } else {
            console.log(this.name + " is not writing any article")
        }
    }
}

// Editor class that receives an assignment, edits it in 3 seconds, and marks it as finished
function Editor(name, manager) {
    
    // Reference to the manager, writer's name, and a busy flag that the manager uses while assigning the article
    this.manager = manager
    this.name = name
    this.busy = false

    this.startEditing = function (assignment) {
        console.log(this.name + " started editing "" + assignment + """)
        this.assignment = assignment
        this.busy = true

        // 3 s timer to replicate manual action
        setTimeout(() => { this.finishEditing() }, 3000)
    }

    this.finishEditing = function () {
        if (this.busy === true) {
            console.log(this.name + " finished editing "" + this.assignment + """)
            this.manager.notifyEditingComplete(this.assignment)
            this.busy = false
        } else {
            console.log(this.name + " is not editing any article")
        }
    }
}

// The mediator class
function Manager() {
    // Store arrays of workers
    this.editors = []
    this.writers = []

    this.setEditors = function (editors) {
        this.editors = editors
    }
    this.setWriters = function (writers) {
        this.writers = writers
    }

    // Manager receives new assignments via this method
    this.notifyNewAssignment = function (assignment) {
        let availableWriter = this.writers.find(function (writer) {
            return writer.busy === false
        })
        availableWriter.startWriting(assignment)
        return availableWriter
    }

    // Writers call this method to notify they're done writing
    this.notifyWritingComplete = function (assignment) {
        let availableEditor = this.editors.find(function (editor) {
            return editor.busy === false
        })
        availableEditor.startEditing(assignment)
        return availableEditor
    }

    // Editors call this method to notify they're done editing
    this.notifyEditingComplete = function (assignment) {
        console.log(""" + assignment + "" is ready to publish")
    }

}

function run() {
    // Create a manager
    let manager = new Manager()

    // Create workers
    let editors = [
        new Editor("Ed," manager),
        new Editor("Phil," manager),
    ]

    let writers = [
        new Writer("Michael," manager),
        new Writer("Rick," manager),
    ]

    // Attach workers to manager
    manager.setEditors(editors)
    manager.setWriters(writers)

    // Send two assignments to manager
    manager.notifyNewAssignment("var vs let in JavaScript")
    manager.notifyNewAssignment("JS promises")

    /**
     * Output:
     * Michael started writing "var vs let in JavaScript"
     * Rick started writing "JS promises"
     * 
     * After 2s, output:
     * Michael finished writing "var vs let in JavaScript"
     * Ed started editing "var vs let in JavaScript"
     * Rick finished writing "JS promises"
     * Phil started editing "JS promises"
     *
     * After 3s, output:
     * Ed finished editing "var vs let in JavaScript"
     * "var vs let in JavaScript" is ready to publish
     * Phil finished editing "JS promises"
     * "JS promises" is ready to publish
     */

}

run()

Aunque mediator proporciona al diseño de tu aplicación desacoplamiento y una gran flexibilidad, al fin y al cabo es otra clase que debes mantener. Debes evaluar si tu diseño puede beneficiarse realmente de un mediador antes de escribir uno, para no acabar añadiendo una complejidad innecesaria a tu base de código.

También es importante tener en cuenta que, aunque la clase mediator no contenga ninguna lógica de negocio directa, sigue conteniendo mucho código que es crucial para el funcionamiento de tu aplicación y, por tanto, puede volverse rápidamente bastante compleja.

16. Memento

Versionar objetos es otro problema común al que te enfrentarás cuando desarrolles aplicaciones. Hay muchos casos de uso en los que necesitas mantener el historial de un objeto, facilitar las reversiones y, a veces, incluso revertir esas reversiones. Escribir la lógica para este tipo de aplicaciones puede ser difícil.

El patrón de diseño Memento está pensado para resolver este problema fácilmente.

Un memento se considera una instantánea de un objeto en un momento determinado. El patrón de diseño Memento utiliza estos mementos para conservar instantáneas del objeto a medida que se modifica con el tiempo. Cuando necesites volver a una versión anterior, sólo tienes que recuperar el memento correspondiente.

A continuación te explicamos cómo puedes implementarlo en una aplicación de procesamiento de texto:

// The memento class that can hold one snapshot of the Originator class - document
function Text(contents) {
    // Contents of the document
    this.contents = contents

    // Accessor function for contents
    this.getContents = function () {
        return this.contents
    }

    // Helper function to calculate word count for the current document
    this.getWordCount = function () {
        return this.contents.length
    }
}

// The originator class that holds the latest version of the document
function Document(contents) {
    // Holder for the memento, i.e., the text of the document
    this.text = new Text(contents)

    // Function to save new contents as a memento
    this.save = function (contents) {
        this.text = new Text(contents)
        return this.text
    }

    // Function to revert to an older version of the text using a memento
    this.restore = function (text) {
        this.text = new Text(text.getContents())
    }

    // Helper function to get the current memento
    this.getText = function () {
        return this.text
    }

    // Helper function to get the word count of the current document
    this.getWordCount = function () {
        return this.text.getWordCount()
    }
}

// The caretaker class that providers helper functions to modify the document
function DocumentManager(document) {
    // Holder for the originator, i.e., the document
    this.document = document

    // Array to maintain a list of mementos
    this.history = []

    // Add the initial state of the document as the first version of the document
    this.history.push(document.getText())

    // Helper function to get the current contents of the documents
    this.getContents = function () {
        return this.document.getText().getContents()
    }

    // Helper function to get the total number of versions available for the document
    this.getVersionCount = function () {
        return this.history.length
    }

    // Helper function to get the complete history of the document
    this.getHistory = function () {
        return this.history.map(function (element) {
            return element.getContents()
        })

    }

    // Function to overwrite the contents of the document
    this.overwrite = function (contents) {
        let newVersion = this.document.save(contents)
        this.history.push(newVersion)
    }

    // Function to append new content to the existing contents of the document
    this.append = function (contents) {
        let currentVersion = this.history[this.history.length - 1]
        let newVersion
        if (currentVersion === undefined)
            newVersion = this.document.save(contents)
        else
            newVersion = this.document.save(currentVersion.getContents() + contents)
        this.history.push(newVersion)
    }

    // Function to delete all the contents of the document
    this.delete = function () {
        this.history.push(this.document.save(""))
    }

    // Function to get a particular version of the document
    this.getVersion = function (versionNumber) {
        return this.history[versionNumber - 1]
    }

    // Function to undo the last change
    this.undo = function () {
        let previousVersion = this.history[this.history.length - 2]
        this.document.restore(previousVersion)
        this.history.push(previousVersion)
    }

    // Function to revert the document to a previous version
    this.revertToVersion = function (version) {
        let previousVersion = this.history[version - 1]
        this.document.restore(previousVersion)
        this.history.push(previousVersion)
    }

    // Helper function to get the total word count of the document
    this.getWordCount = function () {
        return this.document.getWordCount()
    }

}

function run() {
    // Create a document
    let blogPost = new Document("")

    // Create a caretaker for the document
    let blogPostManager = new DocumentManager(blogPost)

    // Change #1: Add some text
    blogPostManager.append("Hello World!")
    console.log(blogPostManager.getContents())
    // Output: Hello World!

    // Change #2: Add some more text
    blogPostManager.append(" This is the second entry in the document")
    console.log(blogPostManager.getContents())
    // Output: Hello World! This is the second entry in the document

    // Change #3: Overwrite the document with some new text
    blogPostManager.overwrite("This entry overwrites everything in the document")
    console.log(blogPostManager.getContents())
    // Output: This entry overwrites everything in the document

    // Change #4: Delete the contents of the document
    blogPostManager.delete()
    console.log(blogPostManager.getContents())
    // Empty output

    // Get an old version of the document
    console.log(blogPostManager.getVersion(2).getContents())
    // Output: Hello World!

    // Change #5: Go back to an old version of the document
    blogPostManager.revertToVersion(3)
    console.log(blogPostManager.getContents())
    // Output: Hello World! This is the second entry in the document

    // Get the word count of the current document
    console.log(blogPostManager.getWordCount())
    // Output: 53

    // Change #6: Undo the last change
    blogPostManager.undo()
    console.log(blogPostManager.getContents())
    // Empty output

    // Get the total number of versions for the document
    console.log(blogPostManager.getVersionCount())
    // Output: 7

    // Get the complete history of the document
    console.log(blogPostManager.getHistory())
    /**
     * Output:
     * [
     *   '',
     *   'Hello World!',
     *   'Hello World! This is the second entry in the document',
     *   'This entry overwrites everything in the document',
     *   '',
     *   'Hello World! This is the second entry in the document',
     *   ''
     * ]
     */
}

run()

Aunque el patrón de diseño Memento es una gran solución para gestionar el historial de un objeto, puede llegar a consumir muchos recursos. Dado que cada memento es casi una copia del objeto, puede inflar la memoria de tu aplicación muy rápidamente si no se utiliza con moderación.

Con un gran número de objetos, la gestión de su ciclo de vida también puede ser una tarea bastante tediosa. Además de todo esto, las clases Originator y Caretaker suelen estar muy acopladas, lo que aumenta la complejidad de tu código base.

17. Observer

El patrón Observer proporciona una solución alternativa al problema de la interacción multiobjeto (visto anteriormente en el patrón Mediator).

En lugar de permitir que cada objeto se comunique entre sí a través de un mediador designado, el patrón Observer les permite observarse mutuamente. Los objetos están diseñados para emitir eventos cuando intentan enviar datos o control, y otros objetos que están «escuchando» estos eventos pueden entonces recibirlos e interactuar basándose en su contenido.

Aquí tienes una demostración sencilla de cómo enviar newsletters a varias personas a través del patrón Observer:

// The newsletter class that can send out posts to its subscribers
function Newsletter() {
   // Maintain a list of subscribers
   this.subscribers = []

   // Subscribe a reader by adding them to the subscribers' list
   this.subscribe = function(subscriber) {
       this.subscribers.push(subscriber)
   }

   // Unsubscribe a reader by removing them from the subscribers' list
   this.unsubscribe = function(subscriber) {
       this.subscribers = this.subscribers.filter(
           function (element) {
               if (element !== subscriber) return element
           }
       )
   }

   // Publish a post by calling the receive function of all subscribers
   this.publish = function(post) {
       this.subscribers.forEach(function(element) {
           element.receiveNewsletter(post)
       })
   }
}

// The reader class that can subscribe to and receive updates from newsletters
function Reader(name) {
   this.name = name

   this.receiveNewsletter = function(post) {
       console.log("Newsletter received by " + name + "!: " + post)
   }

}

function run() {
   // Create two readers
   let rick = new Reader("ed")
   let morty = new Reader("morty")

   // Create your newsletter
   let newsletter = new Newsletter()

   // Subscribe a reader to the newsletter
   newsletter.subscribe(rick)

   // Publish the first post
   newsletter.publish("This is the first of the many posts in this newsletter")
   /**
    * Output:
    * Newsletter received by ed!: This is the first of the many posts in this newsletter
    */

   // Subscribe another reader to the newsletter
   newsletter.subscribe(morty)

   // Publish the second post
   newsletter.publish("This is the second of the many posts in this newsletter")
   /**
    * Output:
    * Newsletter received by ed!: This is the second of the many posts in this newsletter
    * Newsletter received by morty!: This is the second of the many posts in this newsletter
    */

   // Unsubscribe the first reader
   newsletter.unsubscribe(rick)

   // Publish the third post
   newsletter.publish("This is the third of the many posts in this newsletter")
   /**
    * Output:
    * Newsletter received by morty!: This is the third of the many posts in this newsletter
    */

}

run()

Aunque el patrón Observer es una forma hábil de pasar el control y los datos, es más adecuado para situaciones en las que hay un gran número de emisores y receptores que interactúan entre sí a través de un número limitado de conexiones. Si todos los objetos establecieran conexiones uno a uno, perderías la ventaja que obtienes publicando y suscribiéndote a eventos, ya que siempre habrá un único suscriptor por cada publicador (cuando hubiera sido mejor gestionarlo mediante una línea directa de comunicación entre ellos).

Además, el patrón de diseño Observer puede provocar problemas de rendimiento si los eventos de suscripción no se gestionan adecuadamente. Si un objeto continúa suscribiéndose a otro objeto incluso cuando no lo necesita, no podrá optar a la recogida de basura y aumentará el consumo de memoria de la aplicación.

18. State

El patrón de diseño State es uno de los patrones de diseño más utilizados en la industria del desarrollo de software. Los frameworks de JavaScript más populares, como React y Angular, se basan en gran medida en el patrón State para gestionar los datos y el comportamiento de la aplicación en función de esos datos.

En pocas palabras, el patrón de diseño State es útil en situaciones en las que puedes definir estados definitivos de una entidad (que podría ser un componente, una página, una app o una máquina), y la entidad tiene una reacción predefinida al cambio de estado.

Digamos que estás intentando construir un proceso de solicitud de préstamo. Cada paso del proceso de solicitud puede definirse como un estado.

Aunque el cliente suele ver una pequeña lista de estados simplificados de su solicitud (pendiente, en revisión, aceptada y rechazada), puede haber otros pasos implicados internamente. En cada uno de estos pasos, la solicitud se asignará a una persona distinta y puede tener requisitos únicos.

El sistema está diseñado de tal forma que al final de la tramitación en un estado, éste se actualiza al siguiente de la fila, y se inicia el siguiente conjunto de pasos pertinente.

A continuación se explica cómo crear un sistema de gestión de tareas utilizando el patrón de diseño State:

// Create titles for all states of a task
const STATE_TODO = "TODO"
const STATE_IN_PROGRESS = "IN_PROGRESS"
const STATE_READY_FOR_REVIEW = "READY_FOR_REVIEW"
const STATE_DONE = "DONE"

// Create the task class with a title, assignee, and duration of the task
function Task(title, assignee) {
    this.title = title
    this.assignee = assignee

    // Helper function to update the assignee of the task
    this.setAssignee = function (assignee) {
        this.assignee = assignee
    }

    // Function to update the state of the task
    this.updateState = function (state) {

        switch (state) {
            case STATE_TODO:
                this.state = new TODO(this)
                break
            case STATE_IN_PROGRESS:
                this.state = new IN_PROGRESS(this)
                break
            case STATE_READY_FOR_REVIEW:
                this.state = new READY_FOR_REVIEW(this)
                break
            case STATE_DONE:
                this.state = new DONE(this)
                break
            default:
                return
        }
        // Invoke the callback function for the new state after it is set
        this.state.onStateSet()
    }

    // Set the initial state of the task as TODO
    this.updateState(STATE_TODO)
}

// TODO state
function TODO(task) {

    this.onStateSet = function () {
        console.log(task.assignee + " notified about new task "" + task.title + """)
    }
}

// IN_PROGRESS state
function IN_PROGRESS(task) {

    this.onStateSet = function () {
        console.log(task.assignee + " started working on the task "" + task.title + """)
    }
}

// READY_FOR_REVIEW state that updates the assignee of the task to be the manager of the developer
// for the review
function READY_FOR_REVIEW(task) {
    this.getAssignee = function () {
        return "Manager 1"
    }

    this.onStateSet = function () {
        task.setAssignee(this.getAssignee())
        console.log(task.assignee + " notified about completed task "" + task.title + """)
    }
}

// DONE state that removes the assignee of the task since it is now completed
function DONE(task) {
    this.getAssignee = function () {
        return ""
    }

    this.onStateSet = function () {
        task.setAssignee(this.getAssignee())
        console.log("Task "" + task.title + "" completed")
    }
}

function run() {
    // Create a task
    let task1 = new Task("Create a login page," "Developer 1")
    // Output: Developer 1 notified about new task "Create a login page"

    // Set it to IN_PROGRESS
    task1.updateState(STATE_IN_PROGRESS)
    // Output: Developer 1 started working on the task "Create a login page"

    // Create another task
    let task2 = new Task("Create an auth server," "Developer 2")
    // Output: Developer 2 notified about new task "Create an auth server"


    // Set it to IN_PROGRESS as well
    task2.updateState(STATE_IN_PROGRESS)
    // Output: Developer 2 started working on the task "Create an auth server"

    // Update the states of the tasks until they are done
    task2.updateState(STATE_READY_FOR_REVIEW)
    // Output: Manager 1 notified about completed task "Create an auth server"
    task1.updateState(STATE_READY_FOR_REVIEW)
    // Output: Manager 1 notified about completed task "Create a login page"


    task1.updateState(STATE_DONE)
    // Output: Task "Create a login page" completed
    task2.updateState(STATE_DONE)
    // Output: Task "Create an auth server" completed

}

run()

Aunque el patrón State hace un gran trabajo segregando pasos en un proceso, puede llegar a ser extremadamente difícil de mantener en grandes aplicaciones que tienen múltiples estados.

Además, si el diseño de tu proceso permite algo más que moverse linealmente a través de todos los estados, te verás obligado a escribir y mantener más código, ya que cada transición de estado debe gestionarse por separado.

19. Strategy

También conocido como patrón Policy, el patrón Strategy pretende ayudarte a encapsular e intercambiar libremente clases utilizando una interfaz común. Esto ayuda a mantener un acoplamiento débil entre el cliente y las clases y te permite añadir tantas implementaciones como desees.

Se sabe que el patrón Strategy ayuda enormemente en situaciones en las que se necesita la misma operación utilizando diferentes métodos/algoritmos, o cuando hay que sustituir bloques de conmutación masivos por código más amigable.

Aquí tienes un ejemplo del patrón Strategy:

// The strategy class that can encapsulate all hosting providers
function HostingProvider() {
   // store the provider
   this.provider = ""

   // set the provider
   this.setProvider = function(provider) {
       this.provider = provider
   }

   // set the website configuration for which each hosting provider would calculate costs
   this.setConfiguration = function(configuration) {
       this.configuration = configuration
   }

   // the generic estimate method that calls the provider's unique methods to calculate the costs
   this.estimateMonthlyCost = function() {
       return this.provider.estimateMonthlyCost(this.configuration)
   }
}

// Foo Hosting charges for each second and KB of hosting usage
function FooHosting (){
   this.name = "FooHosting"
   this.rate = 0.0000027

   this.estimateMonthlyCost = function(configuration){
       return configuration.duration * configuration.workloadSize * this.rate
   }
}

// Bar Hosting charges per minute instead of seconds
function BarHosting (){
   this.name = "BarHosting"
   this.rate = 0.00018

   this.estimateMonthlyCost = function(configuration){
       return configuration.duration / 60 * configuration.workloadSize * this.rate
   }
}

// Baz Hosting assumes the average workload to be of 10 MB in size
function BazHosting (){
   this.name = "BazHosting"
   this.rate = 0.032

   this.estimateMonthlyCost = function(configuration){
       return configuration.duration * this.rate
   }
}

function run() {

   // Create a website configuration for a website that is up for 24 hours and takes 10 MB of hosting space
   let workloadConfiguration = {
       duration: 84700,
       workloadSize: 10240
   }

   // Create the hosting provider instances
   let fooHosting = new FooHosting()
   let barHosting = new BarHosting()
   let bazHosting = new BazHosting()

   // Create the instance of the strategy class
   let hostingProvider = new HostingProvider()

   // Set the configuration against which the rates have to be calculated
   hostingProvider.setConfiguration(workloadConfiguration)

   // Set each provider one by one and print the rates
   hostingProvider.setProvider(fooHosting)
   console.log("FooHosting cost: " + hostingProvider.estimateMonthlyCost())
   // Output: FooHosting cost: 2341.7856

   hostingProvider.setProvider(barHosting)
   console.log("BarHosting cost: " + hostingProvider.estimateMonthlyCost())
   // Output: BarHosting cost: 2601.9840

   hostingProvider.setProvider(bazHosting)
   console.log("BarHosting cost: " + hostingProvider.estimateMonthlyCost())
   // Output: BarHosting cost: 2710.4000

}

run()

El patrón Strategy es estupendo cuando se trata de introducir nuevas variaciones de una entidad sin cambiar mucho a los clientes. Sin embargo, puede parecer exagerado si sólo tienes que implementar algunas variaciones.

Además, la encapsulación elimina los detalles más sutiles sobre la lógica interna de cada variante, por lo que tu cliente desconoce cómo se va a comportar una variante.

20. Visitor

El patrón Visitor pretende ayudarte a que tu código sea extensible.

La idea es proporcionar un método en la clase que permita a los objetos de otras clases realizar cambios en los objetos de la clase actual con facilidad. Los otros objetos visitan el objeto actual (también llamado objeto lugar), o la clase actual acepta los objetos visitantes, y el objeto lugar gestiona adecuadamente la visita de cada objeto externo.

A continuación te explicamos cómo puedes utilizarlo:

// Visitor class that defines the methods to be called when visiting each place
function Reader(name, cash) {
    this.name = name
    this.cash = cash

    // The visit methods can access the place object and invoke available functions
    this.visitBookstore = function(bookstore) {
        console.log(this.name + " visited the bookstore and bought a book")
        bookstore.purchaseBook(this)
    }

    this.visitLibrary = function() {
        console.log(this.name + " visited the library and read a book")
    }

    // Helper function to demonstrate a transaction
    this.pay = function(amount) {
        this.cash -= amount
    }
}

// Place class for a library
function Library () {
    this.accept = function(reader) {
        reader.visitLibrary()
    }
}

// Place class for a bookstore that allows purchasing book
function Bookstore () {
    this.accept = function(reader) {
        reader.visitBookstore(this)
    }

    this.purchaseBook = function (visitor) {
        console.log(visitor.name + " bought a book")
        visitor.pay(8)
    }
}


function run() {
    // Create a reader (the visitor)
    let reader = new Reader("Rick," 30)

    // Create the places
    let booksInc = new Bookstore()
    let publicLibrary = new Library()

    // The reader visits the library
    publicLibrary.accept(reader)
    // Output: Rick visited the library and read a book
    console.log(reader.name + " has $" + reader.cash)
    // Output: Rick has $30

    // The reader visits the bookstore
    booksInc.accept(reader)
    // Output: Rick visited the bookstore and bought a book
    console.log(reader.name + " has $" + reader.cash)
    // Output: Rick has $22
}

run()

El único defecto de este diseño es que cada clase de visitante debe actualizarse cada vez que se añade o modifica un nuevo lugar. En los casos en que existan varios visitantes y objetos de lugar juntos, esto puede ser difícil de mantener.

Aparte de eso, el método funciona muy bien para mejorar la funcionalidad de las clases de forma dinámica.

Mejores Prácticas para Implementar Patrones de Diseño

Ahora que has visto los patrones de diseño más comunes en JavaScript, aquí tienes algunos consejos que debes tener en cuenta al implementarlos.

Pon Especial Cuidado en Entender si un Patrón se Ajusta a la Solución

Este consejo debe aplicarse antes de implementar un patrón de diseño en tu código fuente. Aunque pueda parecer que un patrón de diseño es el fin de todas tus preocupaciones, tómate un momento para analizar críticamente si eso es cierto.

Hay muchos patrones que resuelven el mismo problema, pero adoptan enfoques diferentes y tienen consecuencias distintas. Así que tu criterio para seleccionar un patrón de diseño no debería ser únicamente si resuelve tu problema o no — también debería ser lo bien que resuelve tu problema y si hay algún otro patrón que pueda presentar una solución más eficiente.

Comprende los Costes de Implantación de un Patrón Antes de Empezar

Aunque los patrones de diseño parecen ser la mejor solución para todos los problemas de ingeniería, no deberías lanzarte a implementarlos en tu código fuente de inmediato.

Al juzgar las consecuencias de implementar una solución, también debes tener en cuenta tu propia situación. ¿Tienes un gran equipo de desarrolladores de software que son expertos en comprender y mantener patrones de diseño? ¿O eres un fundador en fase inicial con un equipo de desarrollo mínimo que busca lanzar un MVP rápido de su producto? Si respondes afirmativamente a la última pregunta, puede que los patrones de diseño no sean la forma de desarrollo más óptima para ti.

Los patrones de diseño no conducen a una gran reutilización del código a menos que se planifiquen en una fase muy temprana del diseño de la aplicación. Utilizar patrones de diseño al azar en varias fases puede dar lugar a una arquitectura de aplicación innecesariamente compleja que tendrías que pasar semanas simplificando.

La eficacia de un patrón de diseño no puede juzgarse mediante ninguna forma de prueba. Son la experiencia y la introspección de tu equipo las que te permitirán saber si funcionan. Si tienes el tiempo y los recursos para dedicar a estos aspectos, sólo entonces los patrones de diseño resolverán realmente tus problemas.

No Conviertas Cada Solución en un Patrón

Otra regla a tener en cuenta es abstenerse de intentar convertir cada pequeño binomio problema-solución en un patrón de diseño y utilizarlo siempre que veas espacio para ello.

Aunque es bueno identificar soluciones estándar y tenerlas en mente cuando te encuentres con problemas similares, hay muchas posibilidades de que el nuevo problema que has encontrado no se ajuste exactamente a la misma descripción que un problema anterior. En tal caso, podrías acabar aplicando una solución subóptima y desperdiciando recursos.

Los patrones de diseño están establecidos hoy como ejemplos principales de binomios problema-solución porque han sido probados por cientos y miles de programadores a lo largo del tiempo y se han generalizado tanto como ha sido posible. Si intentas replicar ese esfuerzo simplemente mirando un montón de problemas y soluciones y llamándolos similares, podrías acabar haciendo mucho más daño a tu código del que esperabas.

¿Cuándo Debes Utilizar Patrones de Diseño?

En resumen, aquí tienes algunas pistas a tener en cuenta para utilizar patrones de diseño. No todas se aplican al desarrollo de todas las aplicaciones, pero deberían darte una buena idea de lo que debes tener en cuenta cuando pienses en utilizar patrones de diseño:

  • Tienes un sólido equipo interno de desarrolladores que entiende bien los patrones de diseño.
  • Sigues un modelo de SDLC que permite discusiones en profundidad sobre la arquitectura de tu aplicación, y los patrones de diseño han surgido en esas discusiones.
  • El mismo conjunto de problemas ha surgido varias veces en tus discusiones sobre diseño, y conoces el patrón de diseño que se ajustará al caso.
  • Has intentado resolver una variación menor de tu problema de forma independiente con el patrón de diseño.
  • Con el patrón de diseño establecido, tu código no parece excesivamente complejo.

Si un patrón de diseño resuelve tu problema y te ayuda a escribir un código sencillo, reutilizable, modular, débilmente acoplado y sin «olor a código», puede que sea el camino correcto.

Otro buen consejo a tener en cuenta es evitar que todo gire en torno a los patrones de diseño. Los patrones de diseño están pensados para ayudarte a resolver problemas. No son leyes que debas acatar ni reglas que debas seguir estrictamente. Las reglas y leyes fundamentales siguen siendo las mismas: mantén tu código limpio, sencillo, legible y escalable. Si un patrón de diseño te ayuda a hacer eso a la vez que resuelve tu problema, deberías estar bien con él.

Resumen

Los patrones de diseño de JavaScript son una forma maravillosa de abordar problemas a los que se han enfrentado muchos programadores a lo largo del tiempo. Presentan soluciones probadas y comprobadas que se esfuerzan por mantener tu código limpio y débilmente acoplado.

Hoy en día, hay cientos de patrones de diseño disponibles que resolverán casi cualquier problema que encuentres al crear aplicaciones. Sin embargo, no todos los patrones de diseño resolverán realmente tu problema siempre.

Al igual que cualquier otra convención de programación, los patrones de diseño deben tomarse como sugerencias para resolver problemas. No son leyes que deban seguirse siempre, y si los tratas como leyes, podrías acabar haciendo mucho daño a tus aplicaciones.

Una vez que tu aplicación esté terminada, necesitarás un lugar donde alojarla — y las soluciones de Alojamiento de Aplicaciones de Kinsta son las más rápidas, fiables y seguras. Sólo tienes que iniciar sesión en tu cuenta MyKinsta (el panel de control administrativo personalizado de Kinsta), conectarte a tu repositorio de GitHub, ¡y lanzarla! Además, sólo se te cobrará por los recursos que utilice tu aplicación.

¿Cuáles son los patrones de diseño que utilizas habitualmente en tu trabajo de programación de software? ¿O hay algún patrón que se nos haya pasado en la lista? ¡Háznoslo saber en los comentarios de abajo!

Kumar Harsh

Kumar is a software developer and a technical author based in India. He specializes in JavaScript and DevOps. You can learn more about his work on his website.