Patrones de diseño para el desarrollo de frontend

Compartir esta publicación

«A Pattern Language: Towns, Buildings, Construction» de Christopher Alexander (1977) es un libro pionero en el campo de la arquitectura. Introdujo el concepto de patrones de diseño, que más tarde influyó en el desarrollo de software. El «Gang of Four» (Erich Gamma, John Vlissides, Ralph Johnson y Richard Helm) amplió esta idea en 1994 en su libro «Design Patterns: Elements of Reusable Object-Oriented Software». Su trabajo presentaba 23 patrones de diseño en el desarrollo de frontend para abordar retos comunes en la programación orientada a objetos.

Desde la aparición del libro, se han descubierto muchos otros patrones orientados a objetos. Sin embargo, el libro se considera una piedra angular en la ingeniería de software porque estableció un vocabulario común para describir las soluciones y proporcionó un enfoque estructurado y reutilizable.

En este artículo, compartiremos algunos de los patrones de diseño utilizados para transformar una base de código heredada en un proyecto de diseño orientado al dominio (DDD). En lugar de limitarnos a cubrir conceptos teóricos, nos sumergiremos en ejemplos prácticos y reales de cómo se pueden aplicar estos patrones de diseño para el desarrollo frontend.

🔎 ¿Necesitas más detalles sobre los patrones de diseño? Echa un vistazo a refactoring.guru para obtener excelentes explicaciones.

Patrones de diseño para el desarrollo de frontend

Patrón Singleton

El patrón Singleton es un patrón de diseño de creación útil cuando necesitas una única fuente de verdad en tus aplicaciones frontales. También puede utilizarse para gestionar estados globales, servicios de enrutamiento y configuraciones de aplicaciones. Garantiza una instancia en toda la aplicación, evitando reinstalaciones y manteniendo la instancia privada y segura. En el siguiente ejemplo, implementamos un singleton para un DevToolsService que actúa como un puente para integrarse con Redux DevTools para facilitar la depuración en la consola de Chrome.

El uso del patrón Singleton garantiza que la conexión DevTools se establezca una vez y se comparta en toda la aplicación, evitando múltiples conexiones redundantes.

Cómo funciona:

  1. #instance es una propiedad estática privada que contiene la instancia única.
  2. getInstance() es un método estático que actúa como un punto de acceso global.
  3. En la primera llamada, getInstance() crea la instancia y la almacena.
  4. En todas las llamadas futuras, getInstance() simplemente devuelve la instancia ya creada – garantizando que el mismo objeto se reutiliza en todas partes.
  5. Por último, exportar por defecto garantiza que siempre que otro módulo importe este servicio, obtendrá la misma instancia preinicializada.
class DevToolsService {
  	static #instance: DevToolsService | null = null;


  	static getInstance(): DevToolsService {
    		if (!DevToolsService.#instance) {
      			DevToolsService.#instance = new DevToolsService();
    		}
   	 	return DevToolsService.#instance;
  	}
}


export default DevToolsService.getInstance();

Patrón Factory Method (Constructor Virtual)

El patrón Factory Method permite delegar la creación de objetos a subclases (o a una clase de fábrica), en lugar de instanciarlos directamente en el flujo principal de la aplicación.

  Crecimiento de la deuda técnica: ¿Cómo puede ocurrir sin darte cuenta?

Por ejemplo, en un sistema de pedidos de pizza, cada tipo de tienda puede tener estados de pedido (fichas) ligeramente diferentes:

  • Las tiendas corporativas pueden tener aplicados descuentos corporativos.
  • Las tiendas que solo hacen entregas podrían tener que esperar a los controles de calidad.

Sin embargo, muchos estados son comunes a todas las tiendas, como:

  • Pedido recibido
  • Preparación
  • Listos
  • Entregado

En este ejemplo, aplicamos el patrón Factory Method para crear diferentes estilos de PizzaChip dependiendo del tipo de tienda (corporativa vs sólo entrega). La clase abstracta (PizzaChip) define la lógica común para todas las tiendas, mientras que la Fábrica (PizzaChipFactory) decide qué clase concreta instanciar. Cada clase concreta (como CorporatePizzaChip) define sólo la parte personalizada de los estilos de chips, manteniendo la lógica de cada tienda aislada y protegida.

export type ChipLabels =
 | 'order_received'
 | 'preparing'
 | 'ready'
 | 'delivered'
 | 'awaiting_quality_check'
 | 'corporate_discount_applied'
 | 'out_for_delivery';


export abstract class PizzaChip {
 private common(): { label: ChipLabels; color: string }[] {
   return [
     { label: 'order_received', color: '#FF7700' },
     { label: 'preparing', color: '#FF7700' },
     { label: 'ready', color: '#FF7700' },
     { label: 'delivered', color: '#FF7700' },
   ];
 }
 protected abstract custom(): { label: ChipLabels; color: string }[];


 getList(): { label: ChipLabels; color: string }[] {
   return this.custom().concat(this.common());
 }
}


export class PizzaChipFactory {
 static create(storeType: string): PizzaChip {
   switch (storeType) {
     case 'corporate':
       return new CorporatePizzaChip();
     default:
       return new DeliveryOnlyPizzaChip();
   }
 }
}


class CorporatePizzaChip extends PizzaChip {
 custom(): { label: ChipLabels; color: string }[] {
   return [
     { label: 'corporate_discount_applied', color: '#FF0000' },
     { label: 'out_for_delivery', color: '#FF0000' },
   ];
 }
}


class DeliveryOnlyPizzaChip extends PizzaChip {
 custom(): { label: ChipLabels; color: string }[] {
   return [{ label: 'awaiting_quality_check', color: '#FF0000' }];
 }
}

Mediante el uso de Factory Method, nos aseguramos de que el chip correcto se crea automáticamente, sin difundir las condiciones específicas de la tienda a través de la base de código. Esto apoya el Principio de Responsabilidad Única y hace que la adición de nuevos tipos de tienda fácil – sólo tienes que añadir una nueva clase y una nueva línea en la fábrica.

Este diseño es especialmente útil en una Arquitectura de Frontend Dirigida por Dominios, donde cada tipo de tienda puede representar su propio Contexto Limitado, con reglas y procesos ligeramente diferentes. Aislando estas reglas en clases separadas y protegiéndolas a través de la fábrica, imponemos límites claros entre contextos. Este enfoque da como resultado un código más limpio, fácil de mantener y preparado para el futuro.

Patrón Abstract Factory

Digamos que para cada tipo de tienda (corporativa frente a sólo entrega), no sólo tiene diferentes PizzaChip, sino también diferentes:

  • Componentes OrderSummary (que muestran diferentes métricas clave como el plazo medio de entrega o los descuentos corporativos aplicados).
  • Notificaciones (redacción diferente en las actualizaciones del estado del pedido enviadas a los clientes).

En ese caso, podrías definir un Abstract Factory como ésta:

interface PizzaChip {
 getList(): { label: string; color: string }[]; 
}
interface StoreNotification {
 getWelcomeMessage(): string; 
}
// The factory interface defines what each type of store factory must be able to create:
interface PizzaStoreFactory {
 createChipLegend(): PizzaChip; 
 createNotification(): StoreNotification; 
}


class CorporateStoreFactory implements PizzaStoreFactory {
 createChipLegend(): PizzaChip {
   return new CorporatePizzaChip();
 }


 createNotification(): StoreNotification {
   return new CorporateNotification();
 }
}
class DeliveryOnlyStoreFactory implements PizzaStoreFactory {
 createChipLegend(): PizzaChip {
   return new DeliveryOnlyPizzaChip();
 }
 createNotification(): StoreNotification {
   return new DeliveryOnlyNotification();
 }
}
//These are the actual implementations of PizzaChip and StoreNotification for each store type:
class CorporatePizzaChip implements PizzaChip {
 getList() {
   return [
     { label: 'order_received', color: '#FF7700' },
     { label: 'corporate_discount_applied', color: '#FF0000' },
   ];
 }
}
class DeliveryOnlyPizzaChip implements PizzaChip {
 getList() {
   return [
     { label: 'order_received', color: '#FF7700' },
     { label: 'awaiting_quality_check', color: '#FF0000' },
   ];
 }
}
class CorporateNotification implements StoreNotification {
 getWelcomeMessage() {
   return 'Welcome to our corporate pizza portal!';
 }
}
class DeliveryOnlyNotification implements StoreNotification {
 getWelcomeMessage() {
   return 'Welcome to our delivery-only pizza tracker!';
 }
}
function createUI(factory: PizzaStoreFactory) {
 const legend = factory.createChipLegend().getList();
 const notification = factory.createNotification().getWelcomeMessage();
 console.log({ legend, notification });
}
// Create UI for corporate store
createUI(new CorporateStoreFactory());
// Create UI for delivery-only store
createUI(new DeliveryOnlyStoreFactory());

Patrón Adapter

El patrón Adapter es un patrón de diseño estructural que permite que dos objetos con interfaces incompatibles trabajen juntos. Se utiliza cuando integramos sistemas heredados con nuevas aplicaciones para reutilizar el código existente sin modificarlo. En este caso, podemos utilizar el Patrón Adapter para crear una clase entre la capa de infraestructura y nuestra llamada a la API. Podemos adaptar lo que esperamos de la API y devolver los datos adaptados.

class OldService {
 getData() {
   return {
     dataPoints: [
       { month: 'January', amount: 100 },
       { month: 'February', amount: 150 },
     ],
   };
 }
}


interface ChartData {
 label: string;
 value: number;
}


class ServiceAdapter {
 constructor(private legacyService: OldService) {}


 getChartData(): ChartData[] {
   const legacyData = this.legacyService.getData();
   return legacyData.dataPoints.map(point => ({
     label: point.month,
     value: point.amount,
   }));
 }
}


const legacyService = new OldService();
const adapter = new ServiceAdapter(legacyService);
console.log(adapter.getChartData());

Patrón Criteria

El patrón Criteria es un patrón de diseño utilizado para filtrar un conjunto de objetos utilizando diferentes criterios y combinarlos lógicamente. Proporciona una forma flexible de realizar filtrados complejos sin saturar el código con múltiples sentencias condicionales.

  Servicios de desarrollo de software a medida

En el desarrollo frontend, es especialmente útil cuando se necesita manejar búsquedas dinámicas, filtros o consultas, sobre todo cuando se trata de formularios, barras de búsqueda o datos paginados.

export class EmailCriteria {
 constructor(
   public sender: string = '',
   public subject: string = '',
   public date: string = ''
 ) {}


 static fromFilters(filters: Partial<EmailCriteria>): EmailCriteria {
   return new EmailCriteria(
     filters.sender ?? '',
     filters.subject ?? '',
     filters.date ?? ''
   );
 }
 }
}


export class EmailService {
 constructor(
   private repository: EmailRepository = new EmailHttpRepository(),
 ) {}


 getEmail(criteria: EmailCriteria): Promise<PaginationResponse<Email>> {
   return this.repository.getEmail(criteria);
 }
}

Patrón Builder

El patrón Builder es un patrón de diseño de creación que se utiliza para construir paso a paso objetos con muchos parámetros opcionales. Separa el proceso de construcción de la representación final, lo que permite utilizar la misma lógica de construcción para crear diferentes configuraciones o versiones de un objeto.

En nuestro dominio, tenemos una entidad llamada Pizza que puede crearse con varias opciones, como Margherita, Cuatro Quesos o Pepperoni. Dado que cada pizza puede tener diferentes ingredientes y configuraciones, necesitamos una forma flexible de construirlas sin escribir código repetitivo para cada variación.

Aquí es donde entra en juego el Patrón Builder. Definimos una interfaz PizzaBuilder que proporciona métodos para añadir diferentes componentes paso a paso, como la salsa, el queso y los ingredientes. Cada método nos permite establecer ingredientes específicos, haciendo que el proceso de construcción sea claro y adaptable.

abstract class PizzaBuilder {
 abstract addSauce(): void;
 abstract addCheese(): void;
 abstract addToppings(toppings: string[]): void;
}


class Pizza {
 ingredients: string[] = [];


 showIngredients() {
   console.log(`Pizza ingredients: ${this.ingredients.join(', ')}`);
 }
}


class MargheritaPizzaBuilder implements PizzaBuilder {
 private pizza: Pizza;


 constructor() {
   this.pizza = new Pizza();
 }


 addSauce() {
   this.pizza.ingredients.push('Tomato Sauce');
 }


 addCheese() {
   this.pizza.ingredients.push('Mozzarella Cheese');
 }


 addToppings(toppings: string[]) {
   this.pizza.ingredients.push(...toppings);
 }


 getPizza(): Pizza {
   const result = this.pizza;
   this.pizza = new Pizza(); // Reset for next pizza
   return result;
 }
}


class PizzaDirector {
 constructor(private builder: PizzaBuilder) {}


 buildSimpleMargherita() {
   this.builder.addSauce();
   this.builder.addCheese();
 }


 buildMargheritaWithExtraToppings() {
   this.builder.addSauce();
   this.builder.addCheese();
   this.builder.addToppings(['Basil', 'Olives']);
 }
}


const builder = new MargheritaPizzaBuilder();
const director = new PizzaDirector(builder);


console.log('🍕 Simple Margherita Pizza:');
director.buildSimpleMargherita();
builder.getPizza().showIngredients();


console.log('\n🍕 Margherita with extra toppings:');
director.buildMargheritaWithExtraToppings();
builder.getPizza().showIngredients();


console.log('\n🍕 Custom Pizza (without director):');
builder.addToppings(['Pepperoni', 'Mushrooms']);
builder.getPizza().showIngredients();

Ventajas clave del uso de patrones de diseño para el desarrollo frontend

Mejor mantenimiento del código Los patrones de diseño para el desarrollo del frontend ofrecen una estructura clara y una separación de preocupaciones, lo que facilita la ampliación, refactorización y depuración del código a lo largo del tiempo.

  Poda de dependencias en el desarrollo de software

Escalabilidad y flexibilidad Patrones como Factory Method y Abstract Factory te permiten añadir nuevas funciones (o variaciones del producto) con cambios mínimos en el código base existente, una necesidad crítica en proyectos frontend de rápida evolución.

Coherencia entre equipos Cuando los equipos siguen patrones bien conocidos, la colaboración resulta más sencilla, la incorporación de nuevos desarrolladores es más rápida y la calidad general del código base mejora.

Mejor capacidad de prueba Los patrones de diseño para el desarrollo del frontend fomentan el diseño modular, lo que simplifica las pruebas unitarias.

Encapsulación de lógica compleja Con patrones como Singleton o Factory, la inicialización compleja, la configuración o la lógica condicional pueden ocultarse dentro del propio patrón, dejando el resto de la aplicación limpia y centrada en la lógica de negocio.

Conclusión

A medida que las aplicaciones de frontend modernas crecen en complejidad, la aplicación de patrones de diseño bien establecidos para el desarrollo frontend se vuelve esencial para escribir código escalable, mantenible y comprensible. Ya se trate de gestionar servicios globales con el patrón Singleton, crear elementos de interfaz de usuario flexibles con el método Factory, garantizar familias de objetos coherentes con Abstract Factory, filtrar datos de forma eficaz con el patrón Criteria, transformar interfaces con el patrón Adapter o construir objetos complejos paso a paso con el patrón Builder, los patrones de diseño proporcionan soluciones probadas a los retos arquitectónicos.

Los patrones de diseño no son sólo conceptos teóricos para los desarrolladores de backend: también aportan beneficios tangibles al desarrollo de frontend, especialmente en aplicaciones a gran escala. Al adoptar estos patrones de diseño para el desarrollo del frontend, los equipos pueden preparar sus bases de código para el futuro, hacer que sus arquitecturas sean más predecibles y, en última instancia, ofrecer experiencias de usuario más fiables y fáciles de mantener.

Author

  • Beste Burcu Bayhan

    Graduated from Istanbul Technical University with a bachelor degree of computer engineering in June 2018. During her studies, she has been involved in projects in various areas including web development, computer vision and computer networks.

    Ver todas las entradas

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Suscríbete a nuestro boletín de noticias

Recibe actualizaciones de los últimos descubrimientos tecnológicos

Acerca de Apiumhub

Apiumhub reúne a una comunidad de desarrolladores y arquitectos de software para ayudarte a transformar tu idea en un producto potente y escalable. Nuestro Tech Hub se especializa en Arquitectura de Software, Desarrollo Web & Desarrollo de Aplicaciones Móviles. Aquí compartimos con usted consejos de la industria & mejores prácticas, basadas en nuestra experiencia.

Estima tu proyecto

Contacta
Posts populares
Obtén nuestro Libro: Software Architecture Metrics

¿Tienes un proyecto desafiante?

Podemos trabajar juntos

apiumhub software development projects barcelona
Secured By miniOrange