Quando se trata de escrever código limpo, sustentável e eficiente, os padrões de design desempenham um papel crucial no mundo do desenvolvimento de software. Os padrões de projeto são soluções reutilizáveis para problemas comuns que os desenvolvedores enfrentam ao projetar e construir sistemas de software. Eles fornecem uma abordagem estruturada para resolver desafios específicos, facilitando a criação de código que não é apenas robusto, mas também mais fácil de entender e manter.
Na Programação Orientada a Objetos (OOP ), os padrões de design servem como diretrizes para estruturar seu código de uma forma que promova flexibilidade, reutilização e escalabilidade. Eles encapsulam as melhores práticas e princípios de design que evoluíram e foram transformados em soluções comprovadas.
Os padrões de design podem ser categorizados em três grupos principais:
Padrões de Criação: Esses padrões se concentram em mecanismos de criação de objetos, tentando criar objetos de maneira adequada à situação. Eles abstraem o processo de instanciação, tornando-o mais flexível e independente do sistema.
Padrões Estruturais: Os padrões estruturais lidam com a composição de objetos, formando relacionamentos entre objetos para criar estruturas maiores e mais complexas. Eles ajudam a definir como objetos e classes podem ser combinados para formar novas estruturas e fornecer novas funcionalidades.
Padrões Comportamentais: Os padrões comportamentais preocupam-se com a comunicação entre objetos, definindo como eles interagem e distribuem responsabilidades. Esses padrões ajudam a projetar sistemas onde os objetos colaboram de maneira mais flexível e eficiente.
Aqui está uma lista de alguns padrões de design comuns em cada categoria:
Padrão Observador: Define uma dependência um-para-muitos entre objetos, de modo que quando um objeto muda de estado, todos os seus dependentes são notificados e atualizados automaticamente.
Padrão de Estratégia: Define uma família de algoritmos, encapsula cada um deles e os torna intercambiáveis.
Padrão de Comando: Encapsula uma solicitação como um objeto, permitindo assim a parametrização de clientes com filas, solicitações e operações.
Padrão de Estado: Permite que um objeto altere seu comportamento quando seu estado interno muda, agrupando o comportamento em classes separadas.
Padrão de Cadeia de Responsabilidade: Passa a solicitação ao longo de uma cadeia de manipuladores, permitindo que cada manipulador decida processar a solicitação ou passá-la para o próximo manipulador na cadeia.
Padrão de Visitante: Representa uma operação a ser realizada nos elementos de uma estrutura de objeto, permitindo definir novas operações sem alterar as classes dos elementos.
Neste blog, nos aprofundaremos em cada um desses padrões de design, fornecendo explicações, casos de uso do mundo real e exemplos de código JavaScript para ajudá-lo a entendê-los e implementá-los de forma eficaz em seus projetos.
O Padrão Singleton é um padrão de design criacional que garante que uma classe tenha apenas uma instância e fornece um ponto global de acesso a essa instância. Esse padrão é especialmente útil quando você deseja limitar o número de instâncias de uma classe em seu aplicativo e controlar o acesso a uma única instância compartilhada.
Em JavaScript, implementar o padrão Singleton é relativamente simples, graças à flexibilidade da linguagem. Vamos mergulhar em um exemplo simples de como criar um Singleton em 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)
Neste exemplo, criamos uma classe Singleton com um construtor que verifica se já existe uma instância. Se uma instância não existir, ele cria uma e a atribui à variável de instância. As chamadas subsequentes ao construtor retornam a instância existente, garantindo que haja apenas uma instância da classe Singleton.
O padrão Singleton é útil em vários cenários, incluindo:
Embora o Padrão Singleton possa ser benéfico, é essencial usá-lo criteriosamente. O uso excessivo do padrão Singleton pode levar a um código fortemente acoplado e ao estado global, o que pode dificultar a manutenção e o teste do seu aplicativo. Portanto, é crucial pesar os prós e os contras e aplicar o padrão onde ele realmente agrega valor à sua base de código.
O Factory Pattern e o Abstract Factory Pattern são padrões de design criacionais que lidam com a criação de objetos, mas o fazem de maneiras diferentes e servem a propósitos distintos. Vamos explorar cada um desses padrões e ver como eles podem ser implementados em JavaScript.
O Factory Pattern é um padrão de criação que fornece uma interface para a criação de objetos, mas permite que as subclasses alterem o tipo de objetos que serão criados. Ele encapsula o processo de criação de objetos, tornando-o mais flexível e desacoplado do código do 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'
Neste exemplo, ProductFactory é responsável por criar instâncias da classe Product. Abstrai o processo de criação, permitindo criar diferentes tipos de produtos ampliando a fábrica.
O Abstract Factory Pattern é outro padrão de criação que fornece uma interface para criar famílias de objetos relacionados ou dependentes sem especificar suas classes concretas. Permite criar conjuntos de objetos que funcionam juntos de forma harmoniosa.
// 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'
Neste exemplo, temos duas fábricas concretas, MacFactory e WindowsFactory, cada uma capaz de criar um conjunto de componentes de UI relacionados (botões e caixas de seleção) para suas respectivas plataformas. A função createUI permite criar uma UI coesa para uma plataforma específica usando a fábrica apropriada.
Quando usar qual padrão:
O Builder Pattern é um padrão de design criacional que separa a construção de um objeto complexo de sua representação, permitindo que o mesmo processo de construção crie diferentes representações. Esse padrão é especialmente útil quando você tem um objeto com um grande número de propriedades e deseja simplificar a criação de instâncias enquanto mantém a flexibilidade.
Em JavaScript, o Builder Pattern geralmente é implementado usando uma classe ou objeto construtor que orienta a construção passo a passo do objeto complexo. Vamos mergulhar em um exemplo para entender como 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);
Neste exemplo, temos uma classe Product com múltiplas propriedades. A classe ProductBuilder ajuda a criar instâncias de Product fornecendo métodos para definir cada propriedade passo a passo. O encadeamento de métodos permite definir múltiplas propriedades de maneira fluente e legível. Finalmente, o método build retorna a instância do Produto totalmente construída.
O Builder Pattern é benéfico em vários cenários, incluindo:
Embora o Builder Pattern ofereça muitas vantagens, é importante observar que ele adiciona complexidade à sua base de código, especialmente se os objetos que estão sendo construídos forem relativamente simples. Portanto, é essencial avaliar se a complexidade introduzida pelo Builder é justificada para o seu caso de uso específico.
O Prototype Pattern é um padrão de design criacional que permite criar novos objetos copiando um objeto existente, conhecido como protótipo. Promove a criação de objetos sem especificar a classe exata do objeto a ser criado. Este padrão é particularmente útil quando você deseja criar instâncias de objetos complexos de forma eficiente.
Em JavaScript, o Prototype Pattern está intimamente relacionado à propriedade interna prototype
e ao método Object.create()
. Vamos explorar como implementar e usar o padrão Prototype em 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'
Neste exemplo, definimos um objeto vehiclePrototype
com métodos e propriedades comuns a todos os veículos. Usamos Object.create()
para criar novas instâncias (car1 e car2) baseadas neste protótipo. Essas instâncias herdam as propriedades e métodos do protótipo, permitindo criar novos objetos com comportamento compartilhado de forma eficiente.
O Padrão Protótipo é valioso em vários cenários, incluindo:
Embora o Padrão Protótipo seja útil, ele tem algumas considerações:
O Object Pool Pattern é um padrão de design criacional que gerencia um pool de objetos reutilizáveis para minimizar a sobrecarga de criação e destruição de objetos. É especialmente útil quando criar e destruir objetos é caro ou consome muitos recursos. O Object Pool Pattern ajuda a melhorar o desempenho e a utilização de recursos reciclando e reutilizando objetos em vez de criar novos do zero.
Em JavaScript, você pode implementar o Object Pool Pattern usando arrays ou classes de gerenciamento de pool personalizadas. Vamos explorar como esse padrão funciona com um exemplo simples.
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)
Neste exemplo, criamos uma classe ObjectPool que gerencia um pool de objetos. O método create cria novos objetos quando o pool não está cheio, o método reuse recupera um objeto do pool para reutilização e o método release retorna um objeto ao pool para uso futuro.
O Object Pool Pattern é útil em vários cenários, incluindo:
Embora o Object Pool Pattern ofereça benefícios de desempenho, é importante considerar o seguinte:
O Padrão Adaptador é um padrão de projeto estrutural que permite que objetos com interfaces incompatíveis trabalhem juntos. Ele atua como uma ponte entre duas interfaces incompatíveis, tornando-as compatíveis sem alterar seu código-fonte. Esse padrão é especialmente útil quando você precisa integrar ou usar código existente que não atende aos requisitos do seu aplicativo.
Em JavaScript, o Padrão Adaptador pode ser implementado usando classes ou funções que agrupam ou adaptam a interface incompatível. Vamos explorar como implementar e usar o Padrão Adaptador em JavaScript com um exemplo prático.
Suponha que você tenha uma classe existente chamada OldSystem
com um método chamado legacyRequest
:
class OldSystem { legacyRequest() { return 'Data from the legacy system'; } }
Agora, você deseja usar esse sistema legado em seu aplicativo moderno que espera uma interface diferente. Você pode criar uma classe ou função de adaptador 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}`; } }
Agora, você pode usar a classe Adapter para tornar o sistema legado compatível com sua aplicação moderna:
const oldSystem = new OldSystem(); const adapter = new Adapter(oldSystem);
const result = adapter.newRequest(); console.log(result); // Output: 'Adapted: Data from the legacy system'
Neste exemplo, a classe Adapter encapsula OldSystem e fornece uma nova interface, newRequest, que é compatível com seu aplicativo moderno.
O Padrão Adaptador é valioso em vários cenários, incluindo:
Embora o Padrão Adaptador forneça flexibilidade e compatibilidade, é essencial considerar alguns pontos:
O Decorator Pattern é um padrão de design estrutural que permite adicionar novos comportamentos ou responsabilidades a objetos dinamicamente sem alterar seu código existente. É uma maneira poderosa de estender a funcionalidade dos objetos envolvendo-os com objetos decorativos. Esse padrão promove o princípio de “aberto para extensão, mas fechado para modificação”, facilitando a adição de novos recursos aos objetos sem alterar sua implementação principal.
Em JavaScript, o Decorator Pattern pode ser implementado usando classes e composição de objetos. Vamos explorar como implementar e usar o padrão Decorator em JavaScript com um exemplo prático.
Suponha que você tenha uma classe 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 } }
Você pode então criar instâncias 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
Neste exemplo, temos a classe Coffee representando um café base. As classes MilkDecorator e SugarDecorator são decoradores que embrulham um objeto de café e adicionam o custo do leite e do açúcar, respectivamente, ao custo base.
O Padrão Decorador é valioso em vários cenários, incluindo:
Embora o Padrão Decorador seja versátil, é importante manter algumas considerações em mente:
O Proxy Pattern é um padrão de design estrutural que fornece um substituto ou espaço reservado para outro objeto para controlar o acesso a ele. Ele atua como um intermediário ou wrapper em torno do objeto de destino, permitindo adicionar comportamentos adicionais, controlar o acesso ou atrasar a criação do objeto. O Proxy Pattern é útil em vários cenários, como implementação de carregamento lento, controle de acesso e registro em log.
Em JavaScript, os proxies podem ser criados usando o objeto Proxy
integrado. Vamos explorar como implementar e usar o Proxy Pattern em JavaScript com exemplos práticos.
Suponha que você tenha um objeto que consome muitos recursos e deseja carregar lentamente apenas quando necessário. Você pode usar um proxy para obter carregamento lento:
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();
Neste exemplo, o LazyResourceProxy atua como um substituto para o ExpensiveResource, criando o recurso real somente quando o método fetchData é chamado pela primeira vez.
Você também pode usar proxies para controlar o acesso a objetos e suas propriedades:
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.'
Neste exemplo, o proxy intercepta a operação get e restringe o acesso à propriedade password.
O Padrão Proxy é valioso em vários cenários, incluindo:
Ao usar o Padrão Proxy, tenha em mente as seguintes considerações:
O Padrão Composto é um padrão de design estrutural que permite compor objetos em estruturas semelhantes a árvores para representar hierarquias parte-todo. Ele permite que os clientes tratem objetos individuais e composições de objetos de maneira uniforme. O Padrão Composto é particularmente útil quando você precisa trabalhar com estruturas complexas compostas de objetos menores e relacionados, mantendo uma interface consistente.
Em JavaScript, você pode implementar o Padrão Composto usando classes ou objetos que compartilham uma interface comum, permitindo construir estruturas hierárquicas. Vamos explorar como implementar e usar o Padrão Composto em JavaScript com exemplos práticos.
Suponha que você esteja construindo um aplicativo de design gráfico que precisa trabalhar tanto com formas simples quanto com composições complexas de formas (por exemplo, grupos). Você pode usar o Padrão Composto para representar esta hierarquia:
// 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
Neste exemplo, a classe Graphic serve como interface do componente. A classe Circle representa formas simples, enquanto a classe Group representa composições de formas. Ambas as classes Circle e Group implementam o método draw, permitindo tratá-las uniformemente durante a renderização.
O Padrão Composto é valioso em vários cenários, incluindo:
Ao trabalhar com o Padrão Composto, considere o seguinte:
O Bridge Pattern é um padrão de design estrutural que separa a abstração de um objeto de sua implementação. Permite criar uma ponte entre os dois, permitindo que variem de forma independente. Este padrão é particularmente útil quando você deseja evitar uma ligação permanente entre uma abstração e sua implementação, tornando seu código mais flexível e de fácil manutenção.
Em JavaScript, o Bridge Pattern pode ser implementado usando classes e objetos que fornecem uma interface abstrata para a abstração e diferentes implementações concretas para diversas plataformas ou recursos. Vamos explorar como implementar e usar o Bridge Pattern em JavaScript com exemplos práticos.
Suponha que você esteja criando um aplicativo de desenho que pode renderizar formas em diferentes plataformas, como navegadores da Web e dispositivos móveis. Você pode usar o Bridge Pattern para separar as formas do desenho (abstração) da lógica de renderização (implementação):
// 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
Neste exemplo, a classe Shape representa a abstração (formas a serem desenhadas) e a classe Renderer representa a interface do implementador (lógica de renderização específica da plataforma). Diferentes implementadores concretos (WebRenderer e MobileRenderer) fornecem lógica de renderização para plataformas web e móveis, respectivamente. As classes Circle e Square são abstrações concretas que representam formas.
O Bridge Pattern é valioso em vários cenários, incluindo:
Ao usar o padrão Bridge, considere o seguinte:
O Flyweight Pattern é um padrão de design estrutural que visa reduzir o consumo de memória e melhorar o desempenho através do compartilhamento de partes comuns de objetos. Isso é conseguido separando o estado intrínseco de um objeto (compartilhado e imutável) de seu estado extrínseco (único e dependente do contexto). Esse padrão é particularmente útil quando você tem um grande número de objetos semelhantes e deseja minimizar o consumo de memória.
Em JavaScript, você pode implementar o padrão Flyweight usando classes ou objetos para representar o estado intrínseco compartilhado e o estado extrínseco individual. Vamos explorar como implementar e usar o padrão Flyweight em JavaScript com exemplos práticos.
Suponha que você esteja desenvolvendo um editor de texto que precise exibir uma grande quantidade de texto. Em vez de criar um objeto separado para cada personagem, você pode usar o Flyweight Pattern para compartilhar objetos de personagem quando eles tiverem as mesmas propriedades intrínsecas (por exemplo, fonte e tamanho):
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
Neste exemplo, a classe Character representa caracteres individuais com propriedades intrínsecas como o próprio caractere, fonte e tamanho. A classe CharacterFactory garante que os caracteres com as mesmas propriedades intrínsecas sejam compartilhados em vez de duplicados.
O padrão Flyweight é valioso em vários cenários, incluindo:
Ao usar o padrão Flyweight, considere o seguinte:
O Observer Pattern é um padrão de design comportamental que estabelece uma dependência um-para-muitos entre objetos. Ele permite que um objeto (o sujeito ou observável) notifique vários observadores (ouvintes) sobre mudanças em seu estado ou dados. Esse padrão é comumente usado para implementar sistemas distribuídos de manipulação de eventos, onde as mudanças de estado de um objeto acionam ações em outros objetos dependentes.
Em JavaScript, você pode implementar o Observer Pattern usando classes personalizadas ou recursos integrados, como ouvintes de eventos e o método addEventListener
. Vamos explorar como implementar e usar o Observer Pattern em JavaScript com exemplos práticos.
Suponha que você esteja criando um aplicativo meteorológico e queira que diferentes partes da UI sejam atualizadas quando as condições climáticas mudarem. Você pode usar uma implementação personalizada do Observer Pattern:
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
Neste exemplo, a WeatherStation atua como o sujeito que notifica os observadores (objetos de exibição) quando os dados meteorológicos mudam. Os observadores assinam o assunto usando o método addObserver e implementam o método update para reagir às mudanças.
JavaScript também fornece uma maneira integrada de implementar o Observer Pattern usando ouvintes 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');
Neste exemplo, o NewsPublisher atua como assunto e os assinantes (funções) são adicionados usando o método subscribe. O métodopublishNews notifica os assinantes invocando suas funções com as notícias.
O Padrão Observer é valioso em vários cenários, incluindo:
Ao usar o padrão Observer, considere o seguinte:
O Strategy Pattern é um padrão de design comportamental que permite definir uma família de algoritmos intercambiáveis, encapsular cada um deles e torná-los intercambiáveis. Ele permite que os clientes escolham o algoritmo apropriado dinamicamente em tempo de execução. Esse padrão promove flexibilidade e reutilização, separando o comportamento do algoritmo do contexto que o utiliza.
Em JavaScript, você pode implementar o Strategy Pattern usando objetos ou funções para representar diferentes estratégias e um objeto de contexto que pode alternar entre essas estratégias. Vamos explorar como implementar e usar o Strategy Pattern em JavaScript com exemplos práticos.
Suponha que você esteja desenvolvendo um aplicativo de comércio eletrônico e queira calcular descontos para diferentes tipos de clientes. Você pode usar o Strategy Pattern para encapsular estratégias de desconto:
// 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)
Neste exemplo, definimos duas estratégias de desconto como funções (regularCustomerDiscount e premiumCustomerDiscount). A classe ShoppingCart toma como parâmetro uma estratégia de desconto e calcula o preço total com base na estratégia escolhida.
O Padrão de Estratégia é valioso em vários cenários, incluindo:
Ao usar o Padrão de Estratégia, considere o seguinte:
O Command Pattern é um padrão de design comportamental que transforma uma solicitação ou operação simples em um objeto independente. Ele permite parametrizar objetos com diferentes solicitações, atrasar ou enfileirar a execução de uma solicitação e oferecer suporte a operações que podem ser revertidas. Esse padrão desacopla o remetente de uma solicitação de seu destinatário, facilitando a extensão e a manutenção do código.
Em JavaScript, você pode implementar o Command Pattern usando objetos ou classes para representar comandos e invocadores que executam esses comandos. Vamos explorar como implementar e usar o Command Pattern em JavaScript com exemplos práticos.
Suponha que você esteja desenvolvendo um aplicativo de controle remoto para uma casa inteligente e queira criar uma maneira flexível de controlar vários dispositivos.
Você pode usar o padrão 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)
Neste exemplo, o Padrão de Comando é usado para encapsular as ações de ligar e desligar as luzes. O RemoteControl serve como invocador e comandos concretos (por exemplo, LightOnCommand e LightOffCommand) encapsulam as ações a serem executadas.
O Padrão de Comando é valioso em vários cenários, incluindo:
Ao usar o padrão de comando, considere o seguinte:
O State Pattern é um padrão de design comportamental que permite que um objeto mude seu comportamento quando seu estado interno muda. Ele encapsula estados como classes separadas e delega o comportamento ao objeto de estado atual. Esse padrão ajuda a gerenciar transições de estado complexas e promove o princípio “aberto-fechado”, facilitando a adição de novos estados sem modificar o código existente.
Em JavaScript, você pode implementar o State Pattern usando classes para representar estados e um objeto de contexto que delega seu comportamento ao estado atual. Vamos explorar como implementar e usar o State Pattern em JavaScript com exemplos práticos.
Suponha que você esteja desenvolvendo uma máquina de venda automática que distribui produtos diferentes. O comportamento da máquina de venda automática depende do seu estado atual, como “Pronto”, “Dispensando” ou “Esgotado”. Você pode usar o State Pattern para modelar este comportamento:
// 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."
Neste exemplo, o State Pattern é usado para gerenciar o comportamento de uma máquina de venda automática. Estados como “Pronto” e “Dispensando” são representados como classes separadas, e o contexto (máquina de venda automática) delega seu comportamento ao estado atual.
O State Pattern é valioso em vários cenários, incluindo:
Ao usar o Padrão de Estado, considere o seguinte:
O padrão Chain of Responsibility é um padrão de design comportamental que ajuda a construir uma cadeia de objetos para lidar com uma solicitação. Cada objeto da cadeia tem a oportunidade de processar a solicitação ou passá-la para o próximo objeto da cadeia. Ele desacopla o remetente de uma solicitação de seus destinatários e permite que vários manipuladores estejam na cadeia. Esse padrão promove flexibilidade e extensibilidade, permitindo adicionar ou modificar manipuladores sem afetar o código do cliente.
Em JavaScript, você pode implementar o padrão Chain of Responsibility usando objetos ou classes que representam manipuladores e um cliente que inicia solicitações. Cada manipulador possui uma referência ao próximo manipulador na cadeia. Vamos explorar como implementar e usar o padrão Chain of Responsibility em JavaScript com exemplos práticos.
Suponha que você esteja desenvolvendo um sistema de processamento de pedidos e queira lidar com os pedidos com base no valor total. Você pode usar o Padrão Cadeia de Responsabilidade para criar uma cadeia de manipuladores, cada um responsável pelo processamento de pedidos dentro de uma determinada faixa de preço:
// 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"
Neste exemplo, o Padrão Cadeia de Responsabilidade é usado para lidar com pedidos de diferentes valores. Manipuladores como SmallOrderHandler, MediumOrderHandler e LargeOrderHandler determinam se podem processar um pedido com base no valor do pedido. Se puderem, eles processam; caso contrário, eles passam o pedido para o próximo manipulador da cadeia.
O Padrão Cadeia de Responsabilidade é valioso em vários cenários, incluindo:
Ao usar o padrão Cadeia de Responsabilidade, considere o seguinte:
O Visitor Pattern é um padrão de design comportamental que permite separar um algoritmo da estrutura de objeto na qual ele opera. Ele fornece uma maneira de adicionar novas operações a objetos sem modificar suas classes, facilitando a extensão da funcionalidade para hierarquias de objetos complexas. Esse padrão é especialmente útil quando você possui um conjunto de elementos distintos e deseja realizar diversas operações neles sem modificar seu código.
Em JavaScript, você pode implementar o Visitor Pattern usando funções ou classes para representar visitantes que visitam elementos dentro de uma estrutura de objeto. Vamos explorar como implementar e usar o Visitor Pattern em JavaScript com exemplos práticos.
Suponha que você esteja desenvolvendo um sistema de gerenciamento de conteúdo onde possui diferentes tipos de elementos de conteúdo, como artigos, imagens e vídeos. Você deseja realizar diversas operações, como renderização e exportação, nesses elementos sem modificar suas classes. Você pode usar o Padrão 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); });
Neste exemplo, temos elementos de conteúdo como Artigo, Imagem e Vídeo, e queremos realizar operações de renderização e exportação neles sem modificar suas classes. Conseguimos isso implementando classes de visitantes como RendererVisitor e ExportVisitor que visitam os elementos e realizam as operações desejadas.
O Padrão de Visitante é valioso em vários cenários, incluindo:
Ao usar o Padrão de Visitante, considere o seguinte:
Nesta exploração abrangente de padrões de design em JavaScript, nos aprofundamos em vários padrões que capacitam os desenvolvedores a criar código flexível, sustentável e eficiente. Cada padrão de design aborda problemas específicos e fornece soluções elegantes para desafios comuns de design de software.
Começamos entendendo o conceito fundamental de padrões de design e os categorizamos em três grupos principais: padrões de criação, estruturais e comportamentais. Dentro de cada categoria, examinamos padrões de design populares e apresentamos suas implementações práticas em JavaScript.
Aqui está uma breve recapitulação dos principais padrões de design que abordamos:
Padrões Criacionais: Esses padrões se concentram em mecanismos de criação de objetos, incluindo o Padrão Singleton para garantir uma única instância de uma classe, Padrões Factory e Abstract Factory para criar objetos com fábricas flexíveis, Padrão Builder para construir objetos complexos passo a passo, Padrão Protótipo para clonagem objetos e Object Pool Pattern para reutilização eficiente de objetos.
Padrões Estruturais: Esses padrões lidam com a composição de objetos, fornecendo maneiras de construir estruturas complexas a partir de componentes mais simples. Exploramos o Padrão Adaptador para adaptar interfaces, o Padrão Decorator para adicionar comportamento a objetos dinamicamente, o Padrão Proxy para controlar o acesso a objetos, o Padrão Composto para compor objetos em estruturas de árvore, o Padrão Bridge para separar a abstração da implementação e o Padrão Flyweight. Padrão para minimizar o uso de memória compartilhando estado comum.
Padrões Comportamentais: Esses padrões estão preocupados com a interação e comunicação entre objetos. Cobrimos o Padrão Observer para implementar sistemas distribuídos de manipulação de eventos, o Padrão Strategy para encapsular algoritmos intercambiáveis, o Padrão Command para transformar solicitações em objetos independentes, o Padrão State para gerenciar o comportamento do objeto com base no estado interno, o Padrão Chain of Responsibility para construir um cadeia de manipuladores para processar solicitações e o Visitor Pattern para separar algoritmos de estruturas de objetos.
Os padrões de design são ferramentas valiosas no kit de ferramentas de um desenvolvedor, permitindo a criação de bases de código escaláveis e de fácil manutenção. Compreender e aplicar esses padrões em seus projetos JavaScript permite que você escreva software mais eficiente, adaptável e robusto.
Lembre-se de que os padrões de projeto não são soluções únicas e sua aplicabilidade depende dos requisitos e desafios específicos do seu projeto. Considere cuidadosamente quando e como aplicá-los para obter os melhores resultados.
À medida que você continua a crescer como desenvolvedor JavaScript, dominar esses padrões de design irá capacitá-lo a enfrentar desafios complexos de design de software com confiança e criatividade. Esteja você criando aplicativos da Web, mecanismos de jogos ou qualquer outro software, os padrões de design serão seus aliados na criação de código elegante e de fácil manutenção. Boa codificação!
Também publicado aqui .