Ao construir aplicativos JavaScript, você pode encontrar cenários onde você precisa construir objetos de uma certa forma predefinida ou reutilizar uma classe comum, modificando ou adaptando para vários casos de uso.

É claro que não é conveniente resolver esses problemas repetidamente.

É aqui que os padrões de design JavaScript vêm em seu socorro.

Os padrões de design JavaScript fornecem a você uma maneira estruturada e repetível de resolver problemas que ocorrem comumente no desenvolvimento do JavaScript.

Neste guia, vamos dar uma olhada no que são padrões de design JavaScript e como usá-los em seus aplicativos JavaScript.

O que é um padrão de design JavaScript?

Os padrões de design JavaScript são soluções de template repetíveis para problemas frequentes no desenvolvimento de aplicativos JavaScript.

A ideia é simples: Programadores em todo o mundo, desde o início do desenvolvimento, têm enfrentado conjuntos de problemas recorrentes ao desenvolver aplicativos. Ao longo do tempo, alguns desenvolvedores escolheram documentar formas testadas e comprovadas de lidar com esses problemas para que outros pudessem se referir às soluções com facilidade.

Conforme mais e mais desenvolvedores escolhiam usar essas soluções e reconheciam sua eficiência na solução de seus problemas, eles se tornaram aceitos como uma forma padrão de solução de problemas e receberam o nome de “padrões de design”

À medida que a importância dos padrões de design foi melhor compreendida, eles foram aprimorados e padronizados. A maioria dos padrões de design modernos possui uma estrutura definida, são organizados em várias categorias e são ensinados em cursos relacionados à ciência da computação como tópicos independentes.

Tipos de padrões de design JavaScript

Aqui estão algumas das mais populares classificações de padrões de design JavaScript.

Criativo

Padrões de design criativos são aqueles que ajudam a resolver problemas em torno da criação e gerenciamento de novas instâncias de objetos JavaScript. Pode ser tão simples quanto limitar uma classe a ter apenas um objeto ou tão complexo quanto definir um método intrincado de seleção manual e adicionar cada recurso em um objeto JavaScript.

Alguns exemplos de padrões de design criativo incluem Singleton, Factory, Abstract Factory, e Builder, entre outros.

Estrutural

Os padrões de projeto estrutural são aqueles que ajudam a resolver problemas em torno do gerenciamento da estrutura (ou esquema) dos objetos JavaScript. Estes problemas podem incluir a criação de uma relação entre dois objetos diferentes ou a abstração de algumas características de um objeto para usuários específicos.

Alguns exemplos de padrões de projeto estrutural incluem Adapter, Bridge, Composite, e Facade.

Comportamental

Os padrões de design comportamentais são aqueles que ajudam a resolver problemas relacionados a como o controle (e responsabilidade) é passado entre vários objetos. Esses problemas podem envolver o controle de acesso a uma lista encadeada ou o estabelecimento de uma entidade única que possa controlar o acesso a vários tipos de objetos.

Alguns exemplos de padrões de design comportamentais incluem Command, Iterator, Memento, e Observer.

Concorrência

Os padrões de design de concorrência são aqueles que ajudam a resolver problemas relacionados à execução simultânea de várias threads e tarefas. Esses problemas podem envolver a manutenção de um objeto ativo entre vários objetos disponíveis ou o gerenciamento de vários eventos fornecidos a um sistema, processando a entrada recebida em partes separadas.

Alguns exemplos de padrões de projeto de concorrência incluem objeto ativo, nuclear react e scheduler.

Arquitetura

Padrões de design de arquitetura são aqueles que ajudam a resolver problemas relacionados ao design de software de maneira ampla. Eles estão geralmente relacionados a como projetar seu sistema e garantir alta disponibilidade, mitigar riscos e evitar gargalos de desempenho.

Dois exemplos de padrões de design de arquitetura são o MVC (Model-View-Controller) e o MVVM (Model-View-ViewModel).

Elementos de padrão de design

Quase todos os padrões de design podem ser decompostos em um conjunto de quatro componentes importantes. Eles são:

  • Pattern name: Este é usado para identificar um padrão de design enquanto se comunica com outros usuários. Exemplos incluem “singleton,” “protótipo,” e muito mais.
  • Problem: Isso descreve o objetivo do padrão de design. É uma pequena descrição do problema que o padrão de desenho está tentando resolver. Pode até incluir um exemplo de cenário para explicar melhor o problema. Ele também pode conter uma lista de condições a serem cumpridas para que um padrão de design resolva completamente o problema subjacente.
  • Solution: Esta é a solução para o problema em questão, composta de elementos como classes, métodos, interfaces, etc. É onde está o grosso de um padrão de design – ele envolve relacionamentos, responsabilidades e colaboradores de vários elementos que são claramente definidos.
  • Results: Esta é uma análise de quão bem o padrão conseguiu resolver o problema. Coisas como espaço e uso de tempo são discutidas, juntamente com abordagens alternativas para resolver o mesmo problema.

Se você está procurando aprender mais sobre padrões de design e seu início, MSU tem algum material de estudo sucinto que você pode consultar.

Por que você deve usar padrões de design?

Existem múltiplas razões pelas quais você gostaria de usar padrões de design:

  • Eles são experimentados e testados: Com um padrão de design, você tem uma solução testada e comprovada para o seu problema (desde que o padrão de design se ajuste à descrição do seu problema). Você não precisa perder tempo procurando por soluções alternativas, e você pode ter certeza de que você tem uma solução que cuida da otimização básica de desempenho para você.
  • Eles são fáceis de entender: Os padrões de design são projetados para serem pequenos, simples e fáceis de entender. Você não precisa ser um programador especializado trabalhando em uma indústria específica há décadas para entender qual padrão de design usar. Eles são propositalmente genéricos (não limitados a nenhuma linguagem de programação específica) e podem ser entendidos por qualquer pessoa que tenha habilidades suficientes de resolução de problemas. Isso também ajuda quando há uma mudança de equipe técnica: um código que depende de um padrão de design é mais fácil de entender para qualquer novo desenvolvedor de software.
  • Eles são simples de implementar: A maioria dos padrões de design são muito simples, como você verá mais adiante em nosso artigo. Você não precisa conhecer múltiplos conceitos de programação para implementá-los em seu código.
  • Eles propõem uma arquitetura de código facilmente reutilizável: A reutilização e a organização do código são fortemente encorajadas em toda a indústria de tecnologia, e os padrões de design podem ajudá-lo a alcançar isso. Como esses padrões são uma maneira padrão de resolver problemas, seus designers se esforçaram para garantir que a arquitetura do aplicativo seja reutilizável, flexível e compatível com a maioria das formas de escrita de código.
  • Eles economizam tempo e tamanho do aplicativo: Um dos maiores benefícios de contar com um conjunto padrão de soluções é que eles ajudarão você a economizar tempo ao implementá-los. Existe uma boa chance de que toda à sua equipe de desenvolvimento conheça bem os padrões de design, facilitando o planejamento, a comunicação e a colaboração durante a implementação. Soluções testadas e comprovadas significam que há uma boa chance de você não acabar vazando recursos ou fazendo um desvio ao construir algum recurso, economizando tempo e espaço. Além disso, a maioria das linguagens de programação fornece bibliotecas de modelos padrão que já implementam alguns padrões de design comuns, como Iterator e Observer.

Os 20 principais padrões de design JavaScript para dominar

Agora que você entende do que um padrão de design é composto e por que você precisa deles, vamos mergulhar mais fundo em como alguns dos padrões de design JavaScript mais comumente utilizados podem ser implementados em um aplicativo JavaScript.

Criativo

Vamos começar a discussão com alguns padrões fundamentais e fáceis de aprender, de design criativo.

1. Singleton

O padrão Singleton é um dos padrões de design mais comumente usados em toda a indústria de desenvolvimento de software. O problema que ele visa resolver é manter apenas uma única instância de uma classe. Isso pode ser útil quando se instanciam objetos que consomem muitos recursos, tais como manipuladores de banco de dados.

Aqui está como você pode implementá-lo no JavaScript:

function SingletonFoo() {

   let fooInstance = null;

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

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

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

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

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

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

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

let foo = SingletonFoo();

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

Embora o padrão Singleton cumpra bem seu propósito, é conhecido por dificultar a depuração, pois oculta as dependências e controla o acesso à inicialização ou destruição das instâncias de uma classe.

2. Factory

O Factory method também é um dos padrões de design mais populares. O problema que o Factory method visa resolver é a criação de objetos sem usar o construtor convencional. Em vez disso, ele recebe a configuração (ou descrição) do objeto desejado e retorna o objeto recém-criado.

Aqui está como você pode implementá-lo em JavaScript:

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

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

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

       return dog;
   }
}

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

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

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

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

function run() {

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

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

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

run()

/**
Output:

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


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


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


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

O padrão de design Factory controla como os objetos serão criados e fornece uma maneira rápida de criar novos objetos, além de uma interface uniforme que define as propriedades que seus objetos terão. Você pode adicionar quantas raças de cachorro quiser, mas desde que os métodos e propriedades expostos pelos tipos de raça permaneçam os mesmos, eles funcionarão perfeitamente.

No entanto, observe que o padrão Factory pode levar a um grande número de classes, o que pode ser difícil de gerenciar.

3. Factoryde Resumos

O padrão de design Factory controla como os objetos serão criados e fornece uma maneira rápida de criar novos objetos, além de uma interface uniforme que define as propriedades que seus objetos terão. Você pode adicionar quantas raças de cachorro quiser, desde que os métodos e propriedades expostos pelos tipos de raça permaneçam os mesmos, eles funcionarão perfeitamente.

No entanto, é importante observar que o padrão Factory pode resultar em um grande número de classes, o que pode ser difícil de gerenciar.

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

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

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

       return dog;
   }
}

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

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

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

       return cat;
   }
}

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

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

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

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

function run() {

   let pets = [];

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

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

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

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

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

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

run()

/**
Output:

Type: dog
Breed: labrador
Size: large


Type: dog
Breed: pug
Size: small


Type: cat
Breed: ragdoll
Size: large


Type: cat
Breed: singapura
Size: small

*/

O padrão Abstract Factory facilita a troca de fábricas concretas de forma fácil e ajuda a promover a uniformidade entre as fábricas e os produtos criados. No entanto, pode se tornar difícil introduzir novos tipos de produtos, pois seria necessário fazer alterações em várias classes para acomodar novos métodos/propriedades.

4. Builder

O padrão Builder é um dos padrões de design de criação mais complexos e flexíveis em JavaScript. Ele permite que você construa cada recurso do seu produto passo a passo, fornecendo controle total sobre como seu objeto é construído, ao mesmo tempo, em que abstrai os detalhes internos.

No exemplo abaixo, você verá o padrão de design Builder em ação, juntamente com o Diretor para ajudar a fazer Pizzas!

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

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

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

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

}

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

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

   let pizzaShop = new PizzaShop()

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

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

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

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

}

run()

Você pode combinar o Builder com um Diretor, como mostrado pela classe PizzaShop no exemplo acima, para pré-definir um conjunto de etapas a seguir toda vez que construir uma variante padrão do seu produto, ou seja, uma receita específica para suas pizzas.

A única questão com este padrão de design é que ele é bastante complexo de configurar e manter. No entanto, adicionar novos recursos dessa maneira é mais simples do que com o método Factory.

5. Prototype

O padrão de design Protótipo é uma maneira rápida e simples de criar novos objetos a partir de objetos existentes, clonando.

Um objeto protótipo é criado primeiro, o qual pode ser clonado várias vezes para criar novos objetos. Ele é útil quando instanciar diretamente um objeto é uma operação mais intensiva em recursos em comparação com a criação de uma cópia de um objeto existente.

No exemplo abaixo, você verá como pode usar o padrão Protótipo para criar novos documentos com base em um documento de modelo definido:

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

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

  
}

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

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

       return document
   }
}

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

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

   let prototype = new DocumentPrototype(baseDocument)

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

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

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

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

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

run()

O método Protótipo funciona muito bem em casos em que uma grande parte dos objetos compartilha os mesmos valores ou quando criar um novo objeto do zero é bastante custoso. No entanto, pode parecer excessivo em casos em que você não precisa de mais do que algumas instâncias da classe.

Estrutural

Padrões de projeto estrutural ajudam você a organizar sua lógica de negócios, fornecendo formas testadas e comprovadas de estruturar suas aulas. Há uma variedade de padrões de projeto estrutural que cada um atende a casos de uso único.

6. Adaptador

Um problema comum ao desenvolver aplicativos é permitir a colaboração entre classes incompatíveis.

Um bom exemplo para entender isso é ao manter a compatibilidade retroativa. Se você escrever uma nova versão de uma classe, naturalmente desejará que ela possa ser facilmente usada em todos os lugares onde a versão antiga funcionava. No entanto, se você fizer alterações incompatíveis, como remover ou atualizar métodos que eram cruciais para o funcionamento da versão antiga, poderá acabar com uma classe que precisa que todos os seus clientes sejam atualizados para ser executada.

Nesses casos, o padrão de design Adapter pode ajudar.

O padrão de design Adapter fornece uma abstração que faz a ponte entre os métodos e propriedades da nova classe e os métodos e propriedades da classe antiga. Ele tem a mesma interface da classe antiga, mas contém lógica para mapear os métodos antigos para os novos métodos a fim de executar operações semelhantes. Isso é semelhante a como um adaptador de tomada atua como um intermediário entre uma tomada de estilo americano e uma tomada de estilo europeu.

Aqui está um exemplo:

// Old bot
function Robot() {

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

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

}

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

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

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

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

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

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

   this.sit = robot.sit

}

function run() {

   let robot = new Robot()

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

   robot = new RobotAdapter("my bot")

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

}

run()

A principal questão com este padrão de design é que ele adiciona complexidade ao seu código-fonte. Você já precisava manter duas classes diferentes, e agora você tem outra classe – o Adaptador – para manter.

7. Bridge

Explicando mais sobre o padrão de design Bridge, ele fornece tanto à classe quanto ao cliente interfaces separadas, permitindo que eles funcionem mesmo em casos de interfaces nativas incompatíveis.

Isso ajuda no desenvolvimento de uma interface muito desacoplada entre os dois tipos de objetos. Também auxilia na melhoria da extensibilidade das interfaces e suas implementações, proporcionando máxima flexibilidade.

Veja como você pode usá-lo:

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

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

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

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

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

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

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

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

function AdvancedRemote(device) {

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

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

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

function run() {

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

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

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

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

   tvSimpleRemote.pressVolumeUpKey()
   tvAdvancedRemote.pressVolumeUpKey()

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

   speakerSimpleRemote.pressVolumeDownKey()
   speakerAdvancedRemote.pressVolumeDownKey()

   speakerSimpleRemote.pressVolumeUpKey()
   speakerAdvancedRemote.pressVolumeUpKey()

   speakerAdvancedRemote.pressMuteKey()
}

Como você já deve ter adivinhado, o padrão Bridge aumenta muito a complexidade da base de código. Além disso, a maioria das interfaces geralmente acaba com apenas uma implementação em casos de uso real, então você não se beneficia muito da reusabilidade do código.

8. Composite

O padrão de design Composite ajuda a estruturar e gerenciar facilmente objetos e entidades semelhantes. A ideia básica por trás do padrão Composite é que os objetos e seus contêineres lógicos podem ser representados usando uma única classe abstrata (que pode armazenar dados/métodos relacionados ao objeto e referências a si mesma para o contêiner).

Faz mais sentido usar o padrão Composite quando seu modelo de dados se assemelha a uma estrutura de árvore. No entanto, não tente transformar um modelo de dados não relacionado a uma árvore em um modelo de dados semelhante a uma árvore apenas pelo bem de usar o padrão Composite, pois isso geralmente pode reduzir muito a flexibilidade.

No exemplo abaixo, você verá como pode usar o padrão de design Composite para construir um sistema de embalagem para produtos de eCommerce que também pode calcular o valor total do pedido por embalagem:

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

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

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

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

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

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

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

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

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

       return totalPrice
   }
}

function run() {

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

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

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

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

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

run()

A maior desvantagem de usar o padrão Composite é que as mudanças nas interfaces dos componentes podem ser muito desafiadoras no futuro. Projetar as interfaces leva tempo e esforço, e a natureza semelhante a árvores do modelo de dados pode tornar muito difícil fazer mudanças como você desejar.

9. Decorator

O padrão de design Decorator ajuda a adicionar novas funcionalidades a objetos existentes, simplesmente envolvendo-os dentro de um novo objeto. É semelhante a como você pode envolver uma caixa de presente já embrulhada com um novo papel de embrulho quantas vezes quiser: cada embrulho permite adicionar quantas funcionalidades desejar, o que o torna altamente flexível.

Do ponto de vista técnico, não há herança envolvida, o que proporciona maior liberdade ao projetar a lógica de negócios.

No exemplo abaixo, você verá como o padrão Decorator ajuda a adicionar mais funcionalidades a uma classe Customer padrão:

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

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

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

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

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

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

run()

As desvantagens desse padrão incluem alta complexidade de código, pois não há um padrão definido para adicionar novas funcionalidades usando decoradores. Você pode acabar com muitos decoradores não uniformes e/ou semelhantes no final do ciclo de desenvolvimento do seu software.

Se você não tiver cuidado ao projetar os decoradores, poderá projetar alguns decoradores de forma logicamente dependente de outros. Se isso não for resolvido, remover ou reestruturar os decoradores posteriormente pode causar problemas na estabilidade do seu aplicativo.

10. Facade

Ao construir a maioria dos aplicativos do mundo real, a lógica de negócio geralmente acaba sendo bastante complexa quando você termina. Você pode acabar com vários objetos e métodos envolvidos na execução de operações principais em seu aplicativo. Manter o controle de suas inicializações, dependências, a ordem correta de execução dos métodos, etc., pode ser bastante complicado e propenso a erros se não for feito corretamente.

O padrão de design Facade ajuda a criar uma abstração entre o ambiente que invoca as operações mencionadas acima e os objetos e métodos envolvidos na conclusão dessas operações. Essa abstração abriga a lógica para inicializar os objetos, controlar suas dependências e outras atividades importantes. O ambiente chamador não tem informações sobre como uma operação é executada. Você pode atualizar livremente a lógica sem fazer alterações que quebrem o código do cliente que faz a chamada.

Aqui está como você pode usá-lo em um aplicativo:

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


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

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

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

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

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

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

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

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

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

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

   onlineStore.placeOrder()
}

Uma desvantagem do uso do padrão Facade é que ele adiciona uma camada adicional de abstração entre a lógica do seu negócio e o cliente, exigindo assim manutenção adicional. Na maioria das vezes, isso aumenta a complexidade geral da base de código.

Além disso, a classe Facade torna-se uma dependência obrigatória do funcionamento do seu aplicativo – o que significa que qualquer erro na classe Facade afeta diretamente o funcionamento do seu aplicativo.

11. Flyweight

O padrão de design Flyweight ajuda a resolver problemas que envolvem objetos com componentes repetidos de maneira eficiente em termos de memória, permitindo reutilizar os componentes comuns do pool de objetos. Isso ajuda a reduzir a carga na memória e resulta em tempos de execução mais rápidos.

No exemplo abaixo, uma frase grande é armazenada na memória usando o padrão de design Flyweight. Em vez de armazenar cada caractere à medida que ocorre, o programa identifica o conjunto de caracteres distintos usados para escrever o parágrafo e seus tipos (número ou letra) e constrói “flyweights” reutilizáveis para cada caractere, contendo detalhes de qual caractere e tipo são armazenados.

Em seguida, a array principal armazena apenas uma lista de referências para esses “flyweights” na ordem em que ocorrem na frase, em vez de armazenar uma instância do objeto de caractere sempre que ele ocorre.

Isso reduz pela metade a memória ocupada pela frase. Tenha em mente que esta é uma explicação muito básica de como processadores de texto armazenam texto.

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

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

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

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

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

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

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

   }
}

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

   return chars
}

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

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

}

run()

Como você pode ter percebido, o padrão Flyweight adiciona complexidade ao design de software, não sendo particularmente intuitivo. Portanto, se economizar memória não é uma preocupação urgente para o seu aplicativo, a complexidade adicional do Flyweight pode trazer mais desvantagens do que benefícios.

Além disso, os “flyweights” trocam memória por eficiência de processamento, portanto, se você tiver poucos ciclos de CPU disponíveis, o Flyweight não será uma boa solução para você.

12. Proxy

O padrão Proxy ajuda a substituir um objeto por outro objeto. Em outras palavras, objetos proxy podem ocupar o lugar de objetos reais (pelos quais são um proxy) e controlar o acesso ao objeto. Esses objetos proxy podem ser usados para realizar algumas ações antes ou depois que uma solicitação de invocação seja passada para o objeto real.

No exemplo abaixo, você verá como o acesso a uma instância de banco de dados é controlado por meio de um proxy que realiza algumas verificações básicas de validação nas solicitações antes de permitir que elas sejam processadas:

function DatabaseHandler() {
   const data = {}

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


}

function DatabaseProxy(databaseInstance) {

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

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

       databaseInstance.set(key, val)
   }

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

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

       return databaseInstance.get(key)
   }

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

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

       return databaseInstance.remove(key)
   }

}

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

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

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

   databaseInstance.set("," "something")

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

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

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

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

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

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


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

   proxy.remove("foo")

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

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

}

run()

Este padrão de design é amplamente utilizado na indústria e ajuda a implementar operações pré e pós-execução facilmente. No entanto, assim como qualquer outro padrão de design, ele também adiciona complexidade à sua base de código, portanto, tente não usá-lo se realmente não precisar.

Também é importante lembrar que, como um objeto adicional está envolvido ao fazer chamadas para o seu objeto real, pode haver alguma latência devido às operações de processamento adicionais. Otimizar o desempenho do seu objeto principal agora também envolve otimizar os métodos do seu proxy para obter um melhor desempenho.

Behavioral

Os padrões de design comportamental ajudam a resolver problemas relacionados à forma como os objetos interagem entre si. Isso pode envolver o compartilhamento ou a transferência de responsabilidade/controle entre objetos para concluir operações conjuntas. Também pode envolver a passagem/compartilhamento de dados entre vários objetos da maneira mais eficiente possível.

13. Chain of Responsibility

O padrão Chain of Responsibility é um dos padrões de design comportamentais mais simples. Ele é útil quando você está projetando a lógica para operações que podem ser tratadas por vários manipuladores.

Similar ao funcionamento da escalada de problemas em equipes de suporte, o controle é passado por uma cadeia de manipuladores, e o manipulador responsável por tomar uma ação completa a operação. Esse padrão de design é frequentemente usado no design de interfaces de usuário, onde várias camadas de componentes podem lidar com um evento de entrada do usuário, como um toque ou um deslize.

Abaixo, você verá um exemplo de escalonamento de reclamação usando o padrão Chain of Responsibility. A reclamação será tratada pelos manipuladores com base em sua gravidade:

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

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

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

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

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

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

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

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

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

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

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

run()

O problema óbvio com esse design é que ele é linear, então pode haver uma certa latência ao lidar com uma operação quando um grande número de manipuladores está encadeado uns aos outros.

Manter o controle de todos os manipuladores pode ser outro ponto problemático, pois pode ficar bastante bagunçado após um certo número de manipuladores. A depuração também é um pesadelo, já que cada solicitação pode terminar em um manipulador diferente, tornando difícil padronizar o processo de registro e depuração.

14. Iterator

O padrão Iterator é bastante simples e é amplamente utilizado em quase todas as linguagens de programação orientadas a objetos modernas. Se você se encontrar diante da tarefa de percorrer uma lista de objetos que não são todos do mesmo tipo, os métodos de iteração normais, como loops for, podem se tornar bastante confusos – especialmente se você também estiver escrevendo lógica de negócios dentro deles.

O padrão Iterator pode ajudá-lo a isolar a lógica de iteração e processamento de suas listas da lógica de negócios principal.

Veja como você pode usá-lo em uma lista bastante básica com vários tipos de elementos:

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

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

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

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

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

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

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

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

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

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

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

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

run()

Não é preciso dizer que esse padrão pode ser desnecessariamente complexo para listas sem múltiplos tipos de elementos. Além disso, se houver muitos tipos de elementos em uma lista, pode se tornar difícil gerenciá-los.

A chave é identificar se você realmente precisa de um iterador com base na sua lista e suas possibilidades futuras de mudança. Além disso, o padrão Iterator é útil apenas em listas, e as listas às vezes podem limitar o acesso ao seu modo linear. Outras estruturas de dados podem oferecer benefícios de desempenho ainda maiores.

15. Mediador

Às vezes, o design do seu aplicativo pode exigir que você trabalhe com um grande número de objetos distintos que possuem vários tipos de lógica de negócios e muitas vezes dependem uns dos outros. Lidar com as dependências pode ser complicado, pois você precisa acompanhar como esses objetos trocam dados e controle entre si.

O padrão de design Mediator tem como objetivo ajudá-lo a resolver esse problema, isolando a lógica de interação desses objetos em um objeto separado.

Esse objeto separado é conhecido como o mediador e é responsável por realizar o trabalho por meio de suas classes de nível inferior. Seu cliente ou ambiente de chamada também interagirá com o mediador em vez das classes de nível inferior.

Aqui está um exemplo do padrão de design Mediator em ação:

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

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

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

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

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

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

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

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

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

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

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

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

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

}

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

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

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

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

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

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

}

run()

Embora o mediador forneça ao design do seu aplicativo desacoplamento e muita flexibilidade, no final das contas, é mais uma classe que você precisa manter. Você deve avaliar se o seu design realmente pode se beneficiar de um mediador antes de escrevê-lo, para não acabar adicionando complexidade desnecessária à sua base de código.

Também é importante ter em mente que, mesmo que a classe do mediador não contenha nenhuma lógica de negócio direta, ela ainda contém muito código que é crucial para o funcionamento do seu aplicativo e, portanto, pode se tornar bastante complexa rapidamente.

16. Memento

A criação de versões de objetos é outro problema comum que você enfrentará ao desenvolver aplicativos. Há muitos casos de uso onde você precisa manter o histórico de um objeto, suportar rollbacks fáceis e, até mesmo, suportar a reversão desses rollbacks. Escrever a lógica para tais aplicativos pode ser difícil.

O padrão de design do Memento tem o objetivo de resolver este problema facilmente.

Um memento é considerado uma imagem instantânea de um objeto em um determinado momento no tempo. O padrão de design Memento utiliza esses mementos para preservar instantâneos do objeto à medida que ele é alterado ao longo do tempo. Quando você precisa reverter para uma versão antiga, basta recuperar o memento correspondente.

Aqui está como você pode implementá-lo em um aplicativo de processamento de texto:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    }

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

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

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

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

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

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

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

}

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

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

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

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

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

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

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

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

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

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

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

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

run()

Embora o padrão de design Memento seja uma ótima solução para gerenciar o histórico de um objeto, ele pode consumir muitos recursos. Como cada memento é praticamente uma cópia do objeto, pode aumentar rapidamente o consumo de memória do seu aplicativo se não for usado com moderação.

Com um grande número de objetos, o gerenciamento de seus ciclos de vida também pode ser uma tarefa tediosa. Além de tudo isso, as classes Originator e Caretaker geralmente estão intimamente acopladas, o que adiciona complexidade à sua base de código.

17. Observer

O padrão Observer fornece uma solução alternativa para o problema de interação entre vários objetos (visto anteriormente no padrão Mediator).

Em vez de permitir que cada objeto se comunique entre si por meio de um mediador designado, o padrão Observer permite que eles se observem mutuamente. Os objetos são projetados para emitir eventos quando estão tentando enviar dados ou controle, e outros objetos que estão “ouvindo” esses eventos podem então recebê-los e interagir com base em seu conteúdo.

Aqui está uma demonstração simples de enviar newsletters para várias pessoas usando o padrão Observer:

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

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

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

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

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

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

}

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

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

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

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

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

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

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

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

}

run()

Embora o padrão Observer seja uma maneira elegante de controlar e transmitir dados, ele é mais adequado a situações em que há um grande número de remetentes e destinatários interagindo entre si por meio de um número limitado de conexões. Se os objetos fizessem conexões individuais um a um, perderíamos a vantagem de publicar e se inscrever em eventos, pois sempre haveria apenas um inscrito para cada publicador (quando seria melhor lidar com uma comunicação direta entre eles).

Além disso, o padrão Observer pode levar a problemas de desempenho se os eventos de inscrição não forem tratados adequadamente. Se um objeto continuar se inscrevendo em outro objeto mesmo quando não precisar, ele não será elegível para coleta de lixo e aumentará o consumo de memória do aplicativo.

18. State

O padrão de design State é um dos padrões de design mais utilizados em toda a indústria de desenvolvimento de software. Os populares frameworks JavaScript como React e Angular confiam fortemente no padrão State para gerenciar dados e comportamentos de aplicativos com base nesses dados.

Em termos simples, o padrão de design State é útil em situações em que você pode definir estados definitivos de uma entidade (que pode ser um componente, uma página, um aplicativo ou uma máquina) e a entidade tem uma reação predefinida à mudança de estado.

Digamos que você esteja tentando construir um processo de inscrição para empréstimo. Cada etapa do processo de inscrição pode ser definida como um estado.

Embora o cliente normalmente veja uma pequena lista de estados simplificados de sua inscrição (pendente, em análise, aceito e rejeitado), pode haver outras etapas envolvidas internamente. Em cada uma dessas etapas, a inscrição será atribuída a uma pessoa específica e poderá ter requisitos únicos.

O sistema é projetado de tal forma que, no final do processamento de um estado, o estado é atualizado para o próximo da fila e o próximo conjunto relevante de etapas é iniciado.

Aqui está como você pode construir um sistema de gerenciamento de tarefas usando o padrão de design State:

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

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

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

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

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

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

// TODO state
function TODO(task) {

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

// IN_PROGRESS state
function IN_PROGRESS(task) {

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

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

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

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

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

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

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

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


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

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


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

}

run()

Embora o padrão State faça um ótimo trabalho ao separar as etapas de um processo, pode se tornar extremamente difícil de manter em aplicativos grandes que possuem vários estados.

Além disso, se o seu design de processo permite mais do que apenas mover-se linearmente por todos os estados, você terá que escrever e manter mais código, já que cada transição de estado precisa ser tratada separadamente.

19. Strategy

Também conhecido como padrão Policy, o padrão Strategy tem como objetivo ajudar a encapsular e intercambiar livremente classes usando uma interface comum. Isso ajuda a manter um acoplamento fraco entre o cliente e as classes e permite adicionar quantas implementações forem necessárias.

O padrão Strategy é conhecido por ser extremamente útil em situações em que a mesma operação precisa ser realizada usando métodos/algoritmos diferentes ou quando blocos de switch massivos precisam ser substituídos por um código mais legível.

Aqui está um exemplo do padrão Strategy:

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

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

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

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

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

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

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

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

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

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

function run() {

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

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

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

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

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

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

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

}

run()

O padrão Strategy é excelente quando se trata de introduzir novas variações de uma entidade sem alterar muito os clientes. No entanto, pode parecer exagerado se você tiver apenas algumas variações para implementar.

Além disso, a encapsulação retira detalhes mais específicos sobre a lógica interna de cada variante, fazendo com que o cliente não esteja ciente de como uma variante vai se comportar.

20. Visitor

O padrão Visitor tem como objetivo ajudar a tornar o código extensível.

A ideia é fornecer um método na classe que permite que objetos de outras classes façam alterações nos objetos da classe atual de maneira fácil. Os outros objetos visitam o objeto atual (também chamado de objeto local) ou a classe atual aceita os objetos visitantes, e o objeto local lida com a visita de cada objeto externo de maneira apropriada.

Veja como você pode usá-lo:

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

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

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

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

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

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

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


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

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

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

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

run()

A única falha neste projeto é que cada classe de visitantes precisa ser atualizada sempre que um novo lugar é adicionado ou modificado. Nos casos em que vários visitantes e objetos de colocação existem juntos, isso pode ser difícil de manter.

Fora isso, o método funciona muito bem para melhorar a funcionalidade das aulas dinamicamente.

Melhores práticas para implementar padrões de Design

Agora que você já viu os padrões de design mais comuns em JavaScript, aqui estão algumas dicas que você deve ter em mente ao implementá-los.

Tenha um cuidado especial para entender se um padrão se encaixa na solução

Essa dica deve ser aplicada antes de implementar um padrão de design em seu código-fonte. Embora possa parecer que um padrão de design é a solução para todos os seus problemas, reserve um momento para analisar criticamente se isso é verdadeiro.

Existem muitos padrões que resolvem o mesmo problema, mas seguem abordagens diferentes e têm consequências diferentes. Portanto, seu critério para selecionar um padrão de design não deve se basear apenas em se ele resolve seu problema ou não, mas também em como ele resolve eficientemente seu problema e se há algum outro padrão que possa apresentar uma solução mais eficiente.

Entenda os custos da implementação de um padrão antes de começar

Compreenda os custos de implementar um padrão antes de começar
Embora os padrões de design pareçam ser a melhor solução para todos os problemas de engenharia, você não deve começar a implementá-los em seu código-fonte imediatamente.

Ao avaliar as consequências de implementar uma solução, você também precisa levar em consideração sua própria situação. Você tem uma equipe grande de desenvolvedores de software que estão bem capacitados para entender e manter os padrões de design? Ou você é um fundador em estágio inicial com uma equipe mínima de desenvolvimento, procurando lançar rapidamente um MVP do seu produto? Se a resposta for sim para a última pergunta, os padrões de design podem não ser a forma mais ideal de desenvolvimento para você.

Os padrões de design não levam a uma grande reutilização de código, a menos que sejam planejados em um estágio muito inicial do design do aplicativo. Usar padrões de design aleatoriamente em várias etapas pode levar a uma arquitetura de aplicativo desnecessariamente complexa, que você terá que passar semanas simplificando.

A eficácia de um padrão de design não pode ser avaliada por meio de testes. É a experiência e a introspecção da sua equipe que vão lhe dizer se eles funcionam. Somente se você tiver tempo e recursos para alocar para esses aspectos, os padrões de design realmente resolverão seus problemas.

Não transforme todas as soluções em padrões

Outra regra a ser lembrada é evitar tentar transformar cada par de problema-solução em um padrão de design e usá-lo onde você vê espaço para isso.

Embora seja bom identificar soluções padrão e tê-las em mente ao encontrar problemas semelhantes, existe uma boa chance de que o novo problema que você encontrou não se encaixe na mesma descrição exata de um problema anterior. Nesse caso, você pode acabar implementando uma solução inferior e desperdiçando recursos.

Os padrões de design são considerados hoje como exemplos principais de pares de problemas-soluções porque foram testados por centenas e milhares de programadores ao longo do tempo e foram generalizados o máximo possível. Se você tentar replicar esse esforço apenas olhando para um monte de problemas e soluções e chamando-os de semelhantes, você pode acabar causando muito mais danos ao seu código do que jamais esperava.

Quando você deve usar os padrões de design?

Para resumir, aqui estão algumas dicas a serem observadas ao usar padrões de design. Nem todas se aplicam ao desenvolvimento de todos os aplicativos, mas devem fornecer uma boa ideia do que observar ao pensar em usar padrões de design:

  • Você possui uma equipe interna de desenvolvedores competentes que possui um bom entendimento dos padrões de design.
  • Você está seguindo um modelo de ciclo de vida de desenvolvimento de software (SDLC) que permite discussões aprofundadas sobre a arquitetura do seu aplicativo, e os padrões de design surgiram nessas discussões.
  • O mesmo conjunto de problemas surgiu várias vezes nas discussões de design, e você sabe qual padrão de design se encaixará no caso.
  • Você tentou resolver uma variação menor do problema de forma independente com o padrão de design.
  • Com o padrão de design implementado, seu código não parece excessivamente complexo.

Se um padrão de design resolve o seu problema e ajuda você a escrever código simples, reutilizável, modular, com baixo acoplamento e livre de “code smells” (sinais de código problemático), pode ser a abordagem correta a seguir.

Outra dica importante a ser lembrada é evitar fazer de tudo sobre os padrões de design. Os padrões de design têm como objetivo ajudá-lo a resolver problemas. Eles não são leis a serem seguidas ou regras a serem rigorosamente seguidas. As regras e leis definitivas ainda são as mesmas: mantenha seu código limpo, simples, legível e escalável. Se um padrão de design ajuda você a fazer isso enquanto resolve o seu problema, você está no caminho certo.

Resumo

Os padrões de design em JavaScript são uma maneira maravilhosa de abordar problemas enfrentados por vários programadores ao longo do tempo. Eles apresentam soluções testadas e comprovadas que visam manter sua base de código limpa e com baixo acoplamento.

Hoje em dia, existem centenas de padrões de design disponíveis que resolvem quase qualquer problema que você encontre ao desenvolver aplicativos. No entanto, nem todos os padrões de design realmente resolverão seu problema todas as vezes.

Assim como qualquer outra convenção de programação, os padrões de design devem ser considerados como sugestões para resolver problemas. Eles não são leis a serem seguidas o tempo todo, e se você tratá-los como leis, pode acabar causando muitos danos aos seus aplicativos.

Uma vez que seu aplicativo esteja concluído, você precisará de um lugar para hospedá-lo – e as soluções de hospedagem de aplicativos da Kinsta estão entre as mais rápidas, confiáveis ​​e seguras. Basta fazer login na sua conta MyKinsta (painel administrativo personalizado da Kinsta), conectar-se ao seu repositório do GitHub e lançar! Além disso, você paga apenas pelos recursos que seu aplicativo utiliza.

Quais são os padrões de design que você utiliza regularmente em seu trabalho de programação de software? Ou há algum padrão que deixamos de mencionar na lista? Deixe-nos saber nos comentários abaixo!

 

Kumar Harsh

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