Cuando se trata de escribir código limpio, fácil de mantener y eficiente, los patrones de diseño desempeñan un papel crucial en el mundo del desarrollo de software. Los patrones de diseño son soluciones reutilizables a problemas comunes que enfrentan los desarrolladores al diseñar y construir sistemas de software. Proporcionan un enfoque estructurado para resolver desafíos específicos, lo que facilita la creación de código que no solo sea sólido sino también más fácil de entender y mantener.
En la programación orientada a objetos (OOP ), los patrones de diseño sirven como pautas para estructurar el código de una manera que promueva la flexibilidad, la reutilización y la escalabilidad. Encapsulan las mejores prácticas y principios de diseño que han evolucionado y se han sintetizado en soluciones probadas.
Los patrones de diseño se pueden clasificar en tres grupos principales:
Patrones de creación: estos patrones se centran en los mecanismos de creación de objetos, tratando de crear objetos de una manera adecuada a la situación. Abstraen el proceso de creación de instancias, haciéndolo más flexible e independiente del sistema.
Patrones estructurales: Los patrones estructurales se ocupan de la composición de objetos, formando relaciones entre objetos para crear estructuras más grandes y complejas. Ayudan a definir cómo se pueden combinar objetos y clases para formar nuevas estructuras y proporcionar nuevas funciones.
Patrones de comportamiento: Los patrones de comportamiento se ocupan de la comunicación entre objetos, definiendo cómo interactúan y distribuyen responsabilidades. Estos patrones le ayudan a diseñar sistemas en los que los objetos colaboran de una manera más flexible y eficiente.
Aquí hay una lista de algunos patrones de diseño comunes en cada categoría:
Patrón de observador: define una dependencia de uno a muchos entre objetos, de modo que cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente.
Patrón de estrategia: define una familia de algoritmos, encapsula cada uno y los hace intercambiables.
Patrón de comando: encapsula una solicitud como un objeto, lo que permite la parametrización de clientes con colas, solicitudes y operaciones.
Patrón de estado: permite que un objeto altere su comportamiento cuando cambia su estado interno, envolviendo el comportamiento en clases separadas.
Patrón de cadena de responsabilidad: pasa la solicitud a lo largo de una cadena de controladores, lo que permite que cada controlador decida procesar la solicitud o pasarla al siguiente controlador de la cadena.
Patrón de visitante: representa una operación que se realizará sobre los elementos de una estructura de objeto, lo que le permite definir nuevas operaciones sin cambiar las clases de los elementos.
En este blog, profundizaremos en cada uno de estos patrones de diseño, brindando explicaciones, casos de uso del mundo real y ejemplos de código JavaScript para ayudarlo a comprenderlos e implementarlos de manera efectiva en sus proyectos.
El patrón Singleton es un patrón de diseño creacional que garantiza que una clase tenga solo una instancia y proporciona un punto de acceso global a esa instancia. Este patrón es especialmente útil cuando desea limitar la cantidad de instancias de una clase en su aplicación y controlar el acceso a una única instancia compartida.
En JavaScript, implementar el patrón Singleton es relativamente sencillo, gracias a la flexibilidad del lenguaje. Profundicemos en un ejemplo simple de cómo crear un Singleton en JavaScript.
// Singleton instance let instance = null;
class Singleton { constructor() { if (!instance) { instance = this; // Your initialization code here } else { return instance; } } // Your methods and properties here }// Usage const singletonA = new Singleton(); const singletonB = new Singleton(); console.log(singletonA === singletonB); // Output: true (both variables reference the same instance)
En este ejemplo, creamos una clase Singleton con un constructor que verifica si ya existe una instancia. Si una instancia no existe, crea una y la asigna a la variable de instancia. Las llamadas posteriores al constructor devuelven la instancia existente, lo que garantiza que solo haya una instancia de la clase Singleton.
El patrón Singleton es útil en varios escenarios, que incluyen:
Si bien el patrón Singleton puede ser beneficioso, es esencial utilizarlo con prudencia. El uso excesivo del patrón Singleton puede generar un código estrechamente acoplado y un estado global, lo que puede hacer que su aplicación sea más difícil de mantener y probar. Por lo tanto, es crucial sopesar los pros y los contras y aplicar el patrón donde realmente agregue valor a su código base.
El patrón Factory y el patrón Abstract Factory son patrones de diseño creacional que se ocupan de la creación de objetos, pero lo hacen de diferentes maneras y tienen distintos propósitos. Exploremos cada uno de estos patrones y veamos cómo se pueden implementar en JavaScript.
El patrón Factory es un patrón de creación que proporciona una interfaz para crear objetos pero permite que las subclases alteren el tipo de objetos que se crearán. Encapsula el proceso de creación de objetos, haciéndolo más flexible y desacoplado del código del cliente.
// Product class class Product { constructor(name) { this.name = name; } }
// Factory for creating products class ProductFactory { createProduct(name) { return new Product(name); } }// Usage const factory = new ProductFactory(); const productA = factory.createProduct('Product A'); const productB = factory.createProduct('Product B');console.log(productA.name); // Output: 'Product A' console.log(productB.name); // Output: 'Product B'
En este ejemplo, ProductFactory es responsable de crear instancias de la clase Producto. Abstrae el proceso de creación, permitiéndole crear diferentes tipos de productos ampliando la fábrica.
El patrón Abstract Factory es otro patrón de creación que proporciona una interfaz para crear familias de objetos relacionados o dependientes sin especificar sus clases concretas. Le permite crear conjuntos de objetos que trabajan juntos armoniosamente.
// Abstract Product classes class Button { render() {} }
class Checkbox { render() {} }// Concrete Product classes class MacButton extends Button { render() { return 'Render Mac button'; } }class MacCheckbox extends Checkbox { render() { return 'Render Mac checkbox'; } }class WindowsButton extends Button { render() { return 'Render Windows button'; } }class WindowsCheckbox extends Checkbox { render() { return 'Render Windows checkbox'; } }// Abstract Factory interface class GUIFactory { createButton() {} createCheckbox() {} }// Concrete Factories class MacFactory extends GUIFactory { createButton() { return new MacButton(); } createCheckbox() { return new MacCheckbox(); } }class WindowsFactory extends GUIFactory { createButton() { return new WindowsButton(); } createCheckbox() { return new WindowsCheckbox(); } }// Usage function createUI(factory) { const button = factory.createButton(); const checkbox = factory.createCheckbox(); return { button, checkbox }; }const macUI = createUI(new MacFactory()); console.log(macUI.button.render()); // Output: 'Render Mac button' console.log(macUI.checkbox.render()); // Output: 'Render Mac checkbox'const windowsUI = createUI(new WindowsFactory()); console.log(windowsUI.button.render()); // Output: 'Render Windows button' console.log(windowsUI.checkbox.render()); // Output: 'Render Windows checkbox'
En este ejemplo, tenemos dos fábricas concretas, MacFactory y WindowsFactory, cada una capaz de crear un conjunto de componentes de interfaz de usuario relacionados (botones y casillas de verificación) para sus respectivas plataformas. La función createUI le permite crear una interfaz de usuario coherente para una plataforma específica utilizando la fábrica adecuada.
Cuándo usar qué patrón:
El Patrón Constructor es un patrón de diseño creacional que separa la construcción de un objeto complejo de su representación, permitiendo que el mismo proceso de construcción cree diferentes representaciones. Este patrón es especialmente útil cuando tiene un objeto con una gran cantidad de propiedades y desea simplificar la creación de instancias manteniendo la flexibilidad.
En JavaScript, el patrón de construcción a menudo se implementa utilizando una clase u objeto de construcción que guía la construcción paso a paso del objeto complejo. Profundicemos en un ejemplo para entender cómo funciona.
// Product class with multiple properties class Product { constructor() { this.name = ''; this.price = 0; this.color = 'white'; // ... other properties }
// Additional methods can be defined here }// Builder for creating Product instances class ProductBuilder { constructor() { this.product = new Product(); } setName(name) { this.product.name = name; return this; // Return the builder for method chaining } setPrice(price) { this.product.price = price; return this; } setColor(color) { this.product.color = color; return this; } // Other methods to set additional properties build() { return this.product; // Return the fully constructed product } }// Usage const builder = new ProductBuilder();const productA = builder .setName('Product A') .setPrice(99.99) .setColor('blue') .build();const productB = builder .setName('Product B') .setPrice(49.99) .build();console.log(productA); console.log(productB);
En este ejemplo, tenemos una clase de Producto con múltiples propiedades. La clase ProductBuilder ayuda a crear instancias de Producto proporcionando métodos para configurar cada propiedad paso a paso. El encadenamiento de métodos le permite establecer múltiples propiedades de forma fluida y legible. Finalmente, el método de compilación devuelve la instancia del Producto completamente construida.
El patrón Builder es beneficioso en varios escenarios, que incluyen:
Si bien el patrón Builder ofrece muchas ventajas, es importante tener en cuenta que agrega complejidad a su código base, especialmente si los objetos que se construyen son relativamente simples. Por lo tanto, es esencial evaluar si la complejidad introducida por Builder está justificada para su caso de uso específico.
El patrón prototipo es un patrón de diseño creacional que le permite crear nuevos objetos copiando un objeto existente, conocido como prototipo. Promueve la creación de objetos sin especificar la clase exacta de objeto a crear. Este patrón es particularmente útil cuando desea crear instancias de objetos complejos de manera eficiente.
En JavaScript, el patrón de prototipo está estrechamente relacionado con la propiedad prototype
incorporada y el método Object.create()
. Exploremos cómo implementar y usar el patrón prototipo en JavaScript.
// Prototype object const vehiclePrototype = { init(make, model) { this.make = make; this.model = model; }, getDetails() { return `${this.make} ${this.model}`; }, };
// Create new instances using the prototype const car1 = Object.create(vehiclePrototype); car1.init('Toyota', 'Camry');const car2 = Object.create(vehiclePrototype); car2.init('Honda', 'Civic');console.log(car1.getDetails()); // Output: 'Toyota Camry' console.log(car2.getDetails()); // Output: 'Honda Civic'
En este ejemplo, definimos un objeto vehiclePrototype
con métodos y propiedades comunes a todos los vehículos. Usamos Object.create()
para crear nuevas instancias (car1 y car2) basadas en este prototipo. Estas instancias heredan las propiedades y métodos del prototipo, lo que le permite crear nuevos objetos con comportamiento compartido de manera eficiente.
El patrón prototipo es valioso en varios escenarios, que incluyen:
Si bien el patrón prototipo es útil, tiene algunas consideraciones:
El patrón de grupo de objetos es un patrón de diseño creacional que gestiona un grupo de objetos reutilizables para minimizar la sobrecarga de creación y destrucción de objetos. Es especialmente útil cuando crear y destruir objetos es costoso o requiere muchos recursos. El patrón de grupo de objetos ayuda a mejorar el rendimiento y la utilización de recursos al reciclar y reutilizar objetos en lugar de crear otros nuevos desde cero.
En JavaScript, puede implementar el patrón de grupo de objetos utilizando matrices o clases de administración de grupos personalizados. Exploremos cómo funciona este patrón con un ejemplo simple.
class ObjectPool { constructor(maxSize) { this.maxSize = maxSize; this.pool = []; }
create() { if (this.pool.length < this.maxSize) { // Create a new object and add it to the pool const obj = { /* Your object initialization code here */ }; this.pool.push(obj); return obj; } else { // Pool is full, cannot create more objects console.log('Pool is full. Cannot create more objects.'); return null; } } reuse() { if (this.pool.length > 0) { // Reuse an object from the pool return this.pool.pop(); } else { // Pool is empty, no objects available for reuse console.log('Pool is empty. No objects available for reuse.'); return null; } } release(obj) { // Release an object back to the pool for reuse this.pool.push(obj); } }// Usage const pool = new ObjectPool(5); // Create a pool with a maximum size of 5 objectsconst obj1 = pool.create(); const obj2 = pool.create(); const obj3 = pool.create();pool.release(obj2); // Release obj2 back to the pool for reuseconst obj4 = pool.reuse(); // Reuse an object from the pool (obj2)
En este ejemplo, creamos una clase ObjectPool que administra un grupo de objetos. El método de creación crea nuevos objetos cuando el grupo no está lleno, el método de reutilización recupera un objeto del grupo para reutilizarlo y el método de liberación devuelve un objeto al grupo para uso futuro.
El patrón de grupo de objetos es útil en varios escenarios, que incluyen:
Si bien el patrón de grupo de objetos ofrece beneficios de rendimiento, es importante considerar lo siguiente:
El patrón adaptador es un patrón de diseño estructural que permite que objetos con interfaces incompatibles trabajen juntos. Actúa como puente entre dos interfaces incompatibles, haciéndolas compatibles sin cambiar su código fuente. Este patrón es especialmente útil cuando necesita integrar o utilizar código existente que no se ajusta del todo a los requisitos de su aplicación.
En JavaScript, el patrón de adaptador se puede implementar utilizando clases o funciones que ajustan o adaptan la interfaz incompatible. Exploremos cómo implementar y usar el patrón de adaptador en JavaScript con un ejemplo práctico.
Supongamos que tiene una clase existente llamada OldSystem
con un método llamado legacyRequest
:
class OldSystem { legacyRequest() { return 'Data from the legacy system'; } }
Ahora desea utilizar este sistema heredado en su aplicación moderna que espera una interfaz diferente. Puede crear una clase de adaptador o una función como esta:
class Adapter { constructor(oldSystem) { this.oldSystem = oldSystem; }
newRequest() { const legacyData = this.oldSystem.legacyRequest(); // Adapt the data or perform any necessary transformations return `Adapted: ${legacyData}`; } }
Ahora, puede utilizar la clase Adaptador para hacer que el sistema heredado sea compatible con su aplicación moderna:
const oldSystem = new OldSystem(); const adapter = new Adapter(oldSystem);
const result = adapter.newRequest(); console.log(result); // Output: 'Adapted: Data from the legacy system'
En este ejemplo, la clase Adapter envuelve OldSystem y proporciona una nueva interfaz, newRequest, que es compatible con su aplicación moderna.
El patrón de adaptador es valioso en varios escenarios, que incluyen:
Si bien el patrón de adaptador proporciona flexibilidad y compatibilidad, es esencial considerar algunos puntos:
El patrón Decorador es un patrón de diseño estructural que le permite agregar nuevos comportamientos o responsabilidades a los objetos de forma dinámica sin alterar su código existente. Es una forma poderosa de ampliar la funcionalidad de los objetos envolviéndolos con objetos decorativos. Este patrón promueve el principio de "abierto a la extensión, pero cerrado a la modificación", lo que facilita agregar nuevas funciones a los objetos sin cambiar su implementación principal.
En JavaScript, el patrón Decorador se puede implementar mediante clases y composición de objetos. Exploremos cómo implementar y usar el patrón Decorador en JavaScript con un ejemplo práctico.
Supongamos que tiene una clase base Coffee
:
class Coffee { cost() { return 5; // Base cost of a regular coffee } } Now, you want to add decorators to your coffee to customize it with additional options, such as milk and sugar:
javascript Copy code class MilkDecorator { constructor(coffee) { this.coffee = coffee; } cost() { return this.coffee.cost() + 2; // Adding the cost of milk } }class SugarDecorator { constructor(coffee) { this.coffee = coffee; } cost() { return this.coffee.cost() + 1; // Adding the cost of sugar } }
Luego puedes crear instancias de café decoradas como esta:
const regularCoffee = new Coffee(); const coffeeWithMilk = new MilkDecorator(regularCoffee); const coffeeWithMilkAndSugar = new SugarDecorator(coffeeWithMilk);
console.log(regularCoffee.cost()); // Output: 5 console.log(coffeeWithMilk.cost()); // Output: 7 console.log(coffeeWithMilkAndSugar.cost()); // Output: 8
En este ejemplo, tenemos la clase Café que representa un café base. Las clases MilkDecorator y SugarDecorator son decoradores que envuelven un objeto de café y agregan el costo de la leche y el azúcar, respectivamente, al costo base.
El patrón decorador es valioso en varios escenarios, que incluyen:
Si bien el patrón decorador es versátil, es importante tener en cuenta algunas consideraciones:
El patrón proxy es un patrón de diseño estructural que proporciona un sustituto o marcador de posición para que otro objeto controle el acceso a él. Actúa como intermediario o contenedor alrededor del objeto de destino, lo que le permite agregar comportamientos adicionales, controlar el acceso o retrasar la creación de objetos. El patrón de proxy es útil en varios escenarios, como la implementación de carga diferida, control de acceso y registro.
En JavaScript, los servidores proxy se pueden crear utilizando el objeto Proxy
integrado. Exploremos cómo implementar y usar el patrón Proxy en JavaScript con ejemplos prácticos.
Supongamos que tiene un objeto que consume muchos recursos y desea cargarlo de forma diferida solo cuando sea necesario. Puede utilizar un proxy para lograr una carga diferida:
class ExpensiveResource { constructor() { console.log('Creating an expensive resource...'); }
fetchData() { console.log('Fetching data...'); } }class LazyResourceProxy { constructor() { this.resource = null; } fetchData() { if (!this.resource) { this.resource = new ExpensiveResource(); } this.resource.fetchData(); } }// Usage const lazyResource = new LazyResourceProxy(); // The actual resource is created and data is fetched only when needed lazyResource.fetchData();
En este ejemplo, LazyResourceProxy actúa como sustituto de ExpensiveResource, creando el recurso real solo cuando se llama al método fetchData por primera vez.
También puedes utilizar proxies para controlar el acceso a objetos y sus propiedades:
const user = { username: 'john_doe', password: 'secret123', };
const userProxy = new Proxy(user, { get(target, property) { if (property === 'password') { throw new Error('Access denied to password.'); } return target[property]; }, });console.log(userProxy.username); // Output: 'john_doe' console.log(userProxy.password); // Throws an error: 'Access denied to password.'
En este ejemplo, el proxy intercepta la operación de obtención y restringe el acceso a la propiedad de contraseña.
El patrón de proxy es valioso en varios escenarios, que incluyen:
Al utilizar el patrón proxy, tenga en cuenta las siguientes consideraciones:
El patrón compuesto es un patrón de diseño estructural que le permite componer objetos en estructuras en forma de árbol para representar jerarquías de parte y todo. Permite a los clientes tratar objetos individuales y composiciones de objetos de manera uniforme. El patrón compuesto es particularmente útil cuando necesita trabajar con estructuras complejas formadas por objetos más pequeños y relacionados, manteniendo al mismo tiempo una interfaz coherente.
En JavaScript, puede implementar el patrón compuesto utilizando clases u objetos que comparten una interfaz común, lo que le permite crear estructuras jerárquicas. Exploremos cómo implementar y usar el patrón compuesto en JavaScript con ejemplos prácticos.
Suponga que está creando una aplicación de diseño gráfico que necesita trabajar tanto con formas simples como con composiciones complejas de formas (por ejemplo, grupos). Puede utilizar el patrón compuesto para representar esta jerarquía:
// Component interface class Graphic { draw() {} }
// Leaf class (represents simple shapes) class Circle extends Graphic { constructor() { super(); // Circle-specific properties and methods } draw() { // Draw a circle } }// Composite class (represents groups of shapes) class Group extends Graphic { constructor() { super(); this.graphics = []; } add(graphic) { this.graphics.push(graphic); } draw() { // Draw each graphic in the group this.graphics.forEach((graphic) => graphic.draw()); } }// Usage const circle1 = new Circle(); const circle2 = new Circle(); const group = new Group();group.add(circle1); group.add(circle2);group.draw(); // Draws both circles in the group
En este ejemplo, la clase Gráfico sirve como interfaz del componente. La clase Círculo representa formas simples, mientras que la clase Grupo representa composiciones de formas. Tanto las clases Circle como Group implementan el método de dibujo, lo que le permite tratarlas de manera uniforme al renderizar.
El patrón compuesto es valioso en varios escenarios, entre ellos:
Cuando trabaje con el patrón compuesto, considere lo siguiente:
El Patrón Puente es un patrón de diseño estructural que separa la abstracción de un objeto de su implementación. Le permite crear un puente entre los dos, permitiéndoles variar de forma independiente. Este patrón es particularmente útil cuando desea evitar un vínculo permanente entre una abstracción y su implementación, haciendo que su código sea más flexible y fácil de mantener.
En JavaScript, el patrón Bridge se puede implementar utilizando clases y objetos que proporcionan una interfaz abstracta para la abstracción y diferentes implementaciones concretas para diversas plataformas o características. Exploremos cómo implementar y usar el patrón Bridge en JavaScript con ejemplos prácticos.
Suponga que está creando una aplicación de dibujo que puede representar formas en diferentes plataformas, como navegadores web y dispositivos móviles. Puede utilizar el patrón de puente para separar las formas de dibujo (abstracción) de la lógica de renderizado (implementación):
// Abstraction class Shape { constructor(renderer) { this.renderer = renderer; }
draw() { // Delegating the drawing to the specific renderer this.renderer.renderShape(this); } }// Implementor interface class Renderer { renderShape(shape) {} }// Concrete Implementors class WebRenderer extends Renderer { renderShape(shape) { console.log(`Drawing on the web: ${shape.constructor.name}`); } }class MobileRenderer extends Renderer { renderShape(shape) { console.log(`Drawing on mobile: ${shape.constructor.name}`); } }// Concrete Abstractions (Shapes) class Circle extends Shape { constructor(renderer) { super(renderer); } }class Square extends Shape { constructor(renderer) { super(renderer); } }// Usage const webRenderer = new WebRenderer(); const mobileRenderer = new MobileRenderer();const circle = new Circle(webRenderer); const square = new Square(mobileRenderer);circle.draw(); // Output: Drawing on the web: Circle square.draw(); // Output: Drawing on mobile: Square
En este ejemplo, la clase Shape representa la abstracción (formas que se dibujarán) y la clase Renderer representa la interfaz del implementador (lógica de representación específica de la plataforma). Diferentes implementadores concretos (WebRenderer y MobileRenderer) proporcionan lógica de renderizado para plataformas web y móviles, respectivamente. Las clases Círculo y Cuadrado son abstracciones concretas que representan formas.
El patrón puente es valioso en varios escenarios, que incluyen:
Cuando utilice el patrón Puente, considere lo siguiente:
El patrón Flyweight es un patrón de diseño estructural que tiene como objetivo reducir el consumo de memoria y mejorar el rendimiento al compartir partes comunes de objetos. Lo logra separando el estado intrínseco de un objeto (compartido e inmutable) de su estado extrínseco (único y dependiente del contexto). Este patrón es particularmente útil cuando tiene una gran cantidad de objetos similares y desea minimizar el uso de memoria.
En JavaScript, puede implementar el patrón Flyweight utilizando clases u objetos para representar el estado intrínseco compartido y el estado extrínseco individual. Exploremos cómo implementar y usar el patrón Flyweight en JavaScript con ejemplos prácticos.
Suponga que está desarrollando un editor de texto que necesita mostrar una gran cantidad de texto. En lugar de crear un objeto separado para cada carácter, puede usar el patrón Flyweight para compartir objetos de caracteres cuando tienen las mismas propiedades intrínsecas (por ejemplo, fuente y tamaño):
class Character { constructor(char, font, size) { this.char = char; this.font = font; this.size = size; }
render() { console.log(`Rendering character "${this.char}" in ${this.font}, size ${this.size}`); } }class CharacterFactory { constructor() { this.characters = {}; } getCharacter(char, font, size) { const key = `${char}-${font}-${size}`; if (!this.characters[key]) { this.characters[key] = new Character(char, font, size); } return this.characters[key]; } }// Usage const factory = new CharacterFactory();const charA1 = factory.getCharacter('A', 'Arial', 12); const charA2 = factory.getCharacter('A', 'Arial', 12); const charB = factory.getCharacter('B', 'Times New Roman', 14);charA1.render(); // Output: Rendering character "A" in Arial, size 12 charA2.render(); // Output: Rendering character "A" in Arial, size 12 (shared instance) charB.render(); // Output: Rendering character "B" in Times New Roman, size 14
En este ejemplo, la clase Carácter representa caracteres individuales con propiedades intrínsecas como el carácter en sí, la fuente y el tamaño. La clase CharacterFactory garantiza que los caracteres con las mismas propiedades intrínsecas se compartan en lugar de duplicarse.
El patrón Flyweight es valioso en varios escenarios, que incluyen:
Cuando utilice el patrón Flyweight, considere lo siguiente:
El patrón de observador es un patrón de diseño de comportamiento que establece una dependencia de uno a muchos entre objetos. Permite que un objeto (el sujeto u observable) notifique a múltiples observadores (oyentes) sobre cambios en su estado o datos. Este patrón se usa comúnmente para implementar sistemas de manejo de eventos distribuidos, donde los cambios de estado de un objeto desencadenan acciones en otros objetos dependientes.
En JavaScript, puede implementar el patrón Observer utilizando clases personalizadas o funciones integradas como detectores de eventos y el método addEventListener
. Exploremos cómo implementar y usar el patrón Observer en JavaScript con ejemplos prácticos.
Suponga que está creando una aplicación meteorológica y desea que se actualicen diferentes partes de la interfaz de usuario cuando cambian las condiciones meteorológicas. Puede utilizar una implementación personalizada del patrón Observer:
class WeatherStation { constructor() { this.observers = []; }
addObserver(observer) { this.observers.push(observer); } removeObserver(observer) { const index = this.observers.indexOf(observer); if (index !== -1) { this.observers.splice(index, 1); } } notifyObservers() { this.observers.forEach((observer) => { observer.update(this); }); } setWeatherData(weatherData) { this.weatherData = weatherData; this.notifyObservers(); } }class WeatherDisplay { update(weatherStation) { console.log(`Current weather: ${weatherStation.weatherData}`); } }// Usage const weatherStation = new WeatherStation(); const display1 = new WeatherDisplay(); const display2 = new WeatherDisplay();weatherStation.addObserver(display1); weatherStation.addObserver(display2);weatherStation.setWeatherData('Sunny'); // Both displays update with the new weather data
En este ejemplo, la estación meteorológica actúa como sujeto que notifica a los observadores (objetos de visualización) cuando cambian los datos meteorológicos. Los observadores se suscriben al tema utilizando el método addObserver e implementan el método de actualización para reaccionar a los cambios.
JavaScript también proporciona una forma integrada de implementar el patrón Observer utilizando detectores de eventos:
class NewsPublisher { constructor() { this.subscribers = []; }
subscribe(subscriber) { this.subscribers.push(subscriber); } unsubscribe(subscriber) { const index = this.subscribers.indexOf(subscriber); if (index !== -1) { this.subscribers.splice(index, 1); } } publishNews(news) { this.subscribers.forEach((subscriber) => { subscriber(news); }); } }// Usage const publisher = new NewsPublisher();const subscriber1 = (news) => { console.log(`Subscriber 1 received news: ${news}`); };const subscriber2 = (news) => { console.log(`Subscriber 2 received news: ${news}`); };publisher.subscribe(subscriber1); publisher.subscribe(subscriber2);publisher.publishNews('Breaking News: Important Announcement');
En este ejemplo, NewsPublisher actúa como asunto y los suscriptores (funciones) se agregan mediante el método de suscripción. El método publicarNoticias notifica a los suscriptores invocando sus funciones con las noticias.
El patrón de observador es valioso en varios escenarios, que incluyen:
Al utilizar el patrón de observador, considere lo siguiente:
El patrón de estrategia es un patrón de diseño de comportamiento que le permite definir una familia de algoritmos intercambiables, encapsular cada uno de ellos y hacerlos intercambiables. Permite a los clientes elegir dinámicamente el algoritmo apropiado en tiempo de ejecución. Este patrón promueve la flexibilidad y la reutilización al separar el comportamiento del algoritmo del contexto que lo utiliza.
En JavaScript, puede implementar el patrón de estrategia utilizando objetos o funciones para representar diferentes estrategias y un objeto de contexto que puede cambiar entre estas estrategias. Exploremos cómo implementar y usar el patrón de estrategia en JavaScript con ejemplos prácticos.
Suponga que está desarrollando una aplicación de comercio electrónico y desea calcular descuentos para diferentes tipos de clientes. Puede utilizar el patrón de estrategia para encapsular estrategias de descuento:
// Discount Strategies const regularCustomerDiscount = (amount) => amount * 0.1; // 10% discount const premiumCustomerDiscount = (amount) => amount * 0.2; // 20% discount
// Context class ShoppingCart { constructor(discountStrategy) { this.items = []; this.discountStrategy = discountStrategy; } addItem(item) { this.items.push(item); } calculateTotal() { const subtotal = this.items.reduce((total, item) => total + item.price, 0); return subtotal - this.discountStrategy(subtotal); } }// Usage const regularCustomerCart = new ShoppingCart(regularCustomerDiscount); const premiumCustomerCart = new ShoppingCart(premiumCustomerDiscount);regularCustomerCart.addItem({ name: 'Item 1', price: 50 }); premiumCustomerCart.addItem({ name: 'Item 2', price: 100 });console.log(`Regular Customer Total: $${regularCustomerCart.calculateTotal()}`); // Output: $45 (after 10% discount) console.log(`Premium Customer Total: $${premiumCustomerCart.calculateTotal()}`); // Output: $80 (after 20% discount)
En este ejemplo, definimos dos estrategias de descuento como funciones (regularCustomerDiscount y premiumCustomerDiscount). La clase ShoppingCart toma como parámetro una estrategia de descuento y calcula el precio total en función de la estrategia elegida.
El patrón estratégico es valioso en varios escenarios, que incluyen:
Al utilizar el patrón de estrategia, considere lo siguiente:
El patrón de comando es un patrón de diseño de comportamiento que convierte una solicitud u operación simple en un objeto independiente. Le permite parametrizar objetos con diferentes solicitudes, retrasar o poner en cola la ejecución de una solicitud y admitir operaciones que se pueden deshacer. Este patrón desacopla al remitente de una solicitud de su receptor, lo que facilita la extensión y el mantenimiento del código.
En JavaScript, puede implementar el patrón de comando utilizando objetos o clases para representar comandos e invocadores que ejecutan esos comandos. Exploremos cómo implementar y usar el patrón de comando en JavaScript con ejemplos prácticos.
Suponga que está desarrollando una aplicación de control remoto para una casa inteligente y desea crear una forma flexible de controlar varios dispositivos.
Puedes usar el patrón de comando:
// Command interface class Command { execute() {} }
// Concrete Commands class LightOnCommand extends Command { constructor(light) { super(); this.light = light; } execute() { this.light.turnOn(); } }class LightOffCommand extends Command { constructor(light) { super(); this.light = light; } execute() { this.light.turnOff(); } }// Receiver (Device) class Light { turnOn() { console.log('Light is on.'); } turnOff() { console.log('Light is off.'); } }// Invoker (Remote Control) class RemoteControl { constructor() { this.commands = []; } addCommand(command) { this.commands.push(command); } executeCommands() { this.commands.forEach((command) => { command.execute(); }); } }// Usage const livingRoomLight = new Light(); const kitchenLight = new Light();const livingRoomLightOn = new LightOnCommand(livingRoomLight); const livingRoomLightOff = new LightOffCommand(livingRoomLight); const kitchenLightOn = new LightOnCommand(kitchenLight); const kitchenLightOff = new LightOffCommand(kitchenLight);const remoteControl = new RemoteControl();remoteControl.addCommand(livingRoomLightOn); remoteControl.addCommand(kitchenLightOff);remoteControl.executeCommands(); // Output: "Light is on." (for living room) // Output: "Light is off." (for kitchen)
En este ejemplo, el patrón de comando se utiliza para encapsular las acciones de encender y apagar luces. RemoteControl actúa como invocador y comandos concretos (por ejemplo, LightOnCommand y LightOffCommand) encapsulan las acciones que se ejecutarán.
El patrón de comando es valioso en varios escenarios, que incluyen:
Al utilizar el patrón de comando, considere lo siguiente:
El patrón de estado es un patrón de diseño de comportamiento que permite que un objeto cambie su comportamiento cuando cambia su estado interno. Encapsula estados como clases separadas y delega el comportamiento al objeto de estado actual. Este patrón ayuda a gestionar transiciones de estados complejas y promueve el principio de "abierto-cerrado", lo que facilita agregar nuevos estados sin modificar el código existente.
En JavaScript, puedes implementar el patrón de estado usando clases para representar estados y un objeto de contexto que delega su comportamiento al estado actual. Exploremos cómo implementar y usar el patrón de estado en JavaScript con ejemplos prácticos.
Suponga que está desarrollando una máquina expendedora que dispensa diferentes productos. El comportamiento de la máquina expendedora depende de su estado actual, como "Listo", "Dispensando" o "Agotado". Puede utilizar el patrón de estado para modelar este comportamiento:
// State interface class VendingMachineState { insertMoney() {} ejectMoney() {} selectProduct() {} dispenseProduct() {} }
// Concrete States class ReadyState extends VendingMachineState { constructor(machine) { super(); this.machine = machine; } insertMoney() { console.log('Money inserted.'); this.machine.setState(this.machine.getDispensingState()); } selectProduct() { console.log('Please insert money first.'); } }class DispensingState extends VendingMachineState { constructor(machine) { super(); this.machine = machine; } dispenseProduct() { console.log('Product dispensed.'); this.machine.setState(this.machine.getReadyState()); } }class VendingMachine { constructor() { this.readyState = new ReadyState(this); this.dispensingState = new DispensingState(this); this.currentState = this.readyState; } setState(state) { this.currentState = state; } getReadyState() { return this.readyState; } getDispensingState() { return this.dispensingState; } insertMoney() { this.currentState.insertMoney(); } selectProduct() { this.currentState.selectProduct(); } dispenseProduct() { this.currentState.dispenseProduct(); } }// Usage const vendingMachine = new VendingMachine();vendingMachine.selectProduct(); // Output: "Please insert money first." vendingMachine.insertMoney(); // Output: "Money inserted." vendingMachine.dispenseProduct(); // Output: "Product dispensed."
En este ejemplo, el patrón de estado se utiliza para gestionar el comportamiento de una máquina expendedora. Estados como "Listo" y "Dispensando" se representan como clases separadas y el contexto (máquina expendedora) delega su comportamiento al estado actual.
El patrón de estado es valioso en varios escenarios, que incluyen:
Al utilizar el patrón de estado, considere lo siguiente:
El patrón de cadena de responsabilidad es un patrón de diseño de comportamiento que le ayuda a crear una cadena de objetos para gestionar una solicitud. Cada objeto de la cadena tiene la oportunidad de procesar la solicitud o pasarla al siguiente objeto de la cadena. Desacopla al remitente de una solicitud de sus receptores y permite que haya varios controladores en la cadena. Este patrón promueve la flexibilidad y la extensibilidad al permitirle agregar o modificar controladores sin afectar el código del cliente.
En JavaScript, puede implementar el patrón de cadena de responsabilidad utilizando objetos o clases que representan controladores y un cliente que inicia solicitudes. Cada controlador tiene una referencia al siguiente controlador de la cadena. Exploremos cómo implementar y utilizar el patrón de cadena de responsabilidad en JavaScript con ejemplos prácticos.
Suponga que está desarrollando un sistema de procesamiento de pedidos y desea manejar los pedidos en función de su monto total. Puede utilizar el patrón de cadena de responsabilidad para crear una cadena de gestores, cada uno de los cuales es responsable de procesar pedidos dentro de un determinado rango de precios:
// Handler interface class OrderHandler { constructor() { this.nextHandler = null; }
setNextHandler(handler) { this.nextHandler = handler; } handleOrder(order) { if (this.canHandleOrder(order)) { this.processOrder(order); } else if (this.nextHandler) { this.nextHandler.handleOrder(order); } else { console.log('No handler can process this order.'); } } canHandleOrder(order) {} processOrder(order) {} }// Concrete Handlers class SmallOrderHandler extends OrderHandler { canHandleOrder(order) { return order.amount <= 100; } processOrder(order) { console.log(`Processing small order for ${order.amount}`); } }class MediumOrderHandler extends OrderHandler { canHandleOrder(order) { return order.amount <= 500; } processOrder(order) { console.log(`Processing medium order for ${order.amount}`); } }class LargeOrderHandler extends OrderHandler { canHandleOrder(order) { return order.amount > 500; } processOrder(order) { console.log(`Processing large order for ${order.amount}`); } }// Client class Order { constructor(amount) { this.amount = amount; } }// Usage const smallOrderHandler = new SmallOrderHandler(); const mediumOrderHandler = new MediumOrderHandler(); const largeOrderHandler = new LargeOrderHandler();smallOrderHandler.setNextHandler(mediumOrderHandler); mediumOrderHandler.setNextHandler(largeOrderHandler);const order1 = new Order(80); const order2 = new Order(250); const order3 = new Order(600);smallOrderHandler.handleOrder(order1); // Output: "Processing small order for 80" smallOrderHandler.handleOrder(order2); // Output: "Processing medium order for 250" smallOrderHandler.handleOrder(order3); // Output: "Processing large order for 600"
En este ejemplo, el patrón de cadena de responsabilidad se utiliza para manejar pedidos de diferentes montos. Los controladores como SmallOrderHandler, MediumOrderHandler y LargeOrderHandler determinan cada uno si pueden procesar un pedido en función del monto del pedido. Si pueden, lo tramitan; de lo contrario, pasan la orden al siguiente controlador de la cadena.
El patrón de cadena de responsabilidad es valioso en varios escenarios, entre ellos:
Al utilizar el patrón de cadena de responsabilidad, considere lo siguiente:
El patrón de visitante es un patrón de diseño de comportamiento que le permite separar un algoritmo de la estructura del objeto en el que opera. Proporciona una forma de agregar nuevas operaciones a objetos sin modificar sus clases, lo que facilita la ampliación de la funcionalidad para jerarquías de objetos complejas. Este patrón es especialmente útil cuando tienes un conjunto de elementos distintos y quieres realizar varias operaciones sobre ellos sin modificar su código.
En JavaScript, puede implementar el patrón de visitante utilizando funciones o clases para representar a los visitantes que visitan elementos dentro de una estructura de objeto. Exploremos cómo implementar y usar el patrón de visitante en JavaScript con ejemplos prácticos.
Suponga que está desarrollando un sistema de gestión de contenidos en el que tiene diferentes tipos de elementos de contenido, como artículos, imágenes y vídeos. Desea realizar varias operaciones, como renderizar y exportar, en estos elementos sin modificar sus clases. Puedes utilizar el patrón de visitante:
// Element interface class ContentElement { accept(visitor) {} }
// Concrete Elements class Article extends ContentElement { accept(visitor) { visitor.visitArticle(this); } }class Image extends ContentElement { accept(visitor) { visitor.visitImage(this); } }class Video extends ContentElement { accept(visitor) { visitor.visitVideo(this); } }// Visitor interface class Visitor { visitArticle(article) {} visitImage(image) {} visitVideo(video) {} }// Concrete Visitors class RendererVisitor extends Visitor { visitArticle(article) { console.log(`Rendering article: ${article.title}`); } visitImage(image) { console.log(`Rendering image: ${image.caption}`); } visitVideo(video) { console.log(`Rendering video: ${video.title}`); } }class ExportVisitor extends Visitor { visitArticle(article) { console.log(`Exporting article: ${article.title}`); } visitImage(image) { console.log(`Exporting image: ${image.caption}`); } visitVideo(video) { console.log(`Exporting video: ${video.title}`); } }// Usage const elements = [new Article('Article 1'), new Image('Image 1'), new Video('Video 1')]; const renderer = new RendererVisitor(); const exporter = new ExportVisitor();elements.forEach((element) => { element.accept(renderer); element.accept(exporter); });
En este ejemplo, tenemos elementos de contenido como Artículo, Imagen y Video, y queremos realizar operaciones de renderizado y exportación en ellos sin modificar sus clases. Esto lo logramos implementando clases de visitantes como RendererVisitor y ExportVisitor que visitan los elementos y realizan las operaciones deseadas.
El patrón de visitante es valioso en varios escenarios, que incluyen:
Al utilizar el patrón de visitante, considere lo siguiente:
En esta exploración integral de los patrones de diseño en JavaScript, hemos profundizado en varios patrones que permiten a los desarrolladores crear código flexible, fácil de mantener y eficiente. Cada patrón de diseño aborda problemas específicos y proporciona soluciones elegantes a desafíos comunes de diseño de software.
Comenzamos por comprender el concepto fundamental de patrones de diseño y los categorizamos en tres grupos principales: patrones de creación, estructurales y de comportamiento. Dentro de cada categoría, examinamos patrones de diseño populares y mostramos sus implementaciones prácticas en JavaScript.
Aquí hay un breve resumen de los patrones de diseño clave que cubrimos:
Patrones de creación: estos patrones se centran en los mecanismos de creación de objetos, incluido el patrón Singleton para garantizar una única instancia de una clase, los patrones Factory y Abstract Factory para crear objetos con fábricas flexibles, el patrón Builder para construir objetos complejos paso a paso, el patrón Prototype para clonar. objetos y patrón de grupo de objetos para una reutilización eficiente de objetos.
Patrones estructurales: estos patrones se ocupan de la composición de objetos y proporcionan formas de construir estructuras complejas a partir de componentes más simples. Exploramos el Patrón Adaptador para adaptar interfaces, el Patrón Decorador para agregar comportamiento a objetos dinámicamente, el Patrón Proxy para controlar el acceso a objetos, el Patrón Compuesto para componer objetos en estructuras de árbol, el Patrón Puente para separar la abstracción de la implementación y el Patrón Flyweight. Patrón para minimizar el uso de memoria compartiendo un estado común.
Patrones de comportamiento: estos patrones se refieren a la interacción y comunicación entre objetos. Cubrimos el patrón de observador para implementar sistemas de manejo de eventos distribuidos, el patrón de estrategia para encapsular algoritmos intercambiables, el patrón de comando para convertir solicitudes en objetos independientes, el patrón de estado para administrar el comportamiento de los objetos basado en el estado interno, el patrón de cadena de responsabilidad para construir un cadena de controladores para procesar solicitudes y el patrón de visitante para separar algoritmos de estructuras de objetos.
Los patrones de diseño son herramientas valiosas en el conjunto de herramientas de un desarrollador, que permiten la creación de bases de código escalables y mantenibles. Comprender y aplicar estos patrones en sus proyectos de JavaScript le permite escribir software más eficiente, adaptable y robusto.
Recuerde que los patrones de diseño no son soluciones únicas para todos y su aplicabilidad depende de los requisitos y desafíos específicos de su proyecto. Considere cuidadosamente cuándo y cómo aplicarlos para lograr los mejores resultados.
A medida que siga creciendo como desarrollador de JavaScript, dominar estos patrones de diseño le permitirá afrontar complejos desafíos de diseño de software con confianza y creatividad. Ya sea que esté creando aplicaciones web, motores de juegos o cualquier otro software, los patrones de diseño serán sus aliados para crear código elegante y fácil de mantener. ¡Feliz codificación!
También publicado aquí .