Padrões de design são soluções para problemas recorrentes no desenvolvimento de aplicativos de software.
Como todos sabemos, existem três tipos de padrões de design. Eles são:
- Criativo
- Estrutural
- Comportamental
Mas, espere. o que isso significa?
Padrão de criação se preocupa com a maneira como criamos objetos em um estilo orientado a objetos. ele aplica padrões na maneira como instanciamos uma classe.
O padrão estrutural se preocupa com como nossas classes e objetos são compostos para formar uma estrutura maior em nosso aplicativo.
O padrão comportamental se preocupa em como os objetos podem interagir de maneira eficiente sem estar fortemente acoplados.
Este tutorial explicará a você alguns dos padrões de design mais comuns que você pode usar em seu aplicativo Node.js. usaremos o Typescript para tornar a implementação mais fácil.
Singleton
O padrão singleton implica que deve haver apenas uma instância para uma classe. Em termos leigos, deve haver apenas um presidente para cada país por vez. Seguindo esse padrão, podemos evitar ter várias instâncias para uma classe específica.
Um bom exemplo do padrão Singleton é a conexão com o banco de dados em nosso aplicativo. Ter várias instâncias de um banco de dados em nosso aplicativo torna o aplicativo instável. Portanto, o padrão singleton fornece uma solução para esse problema gerenciando uma única instância no aplicativo.
Vamos ver como implementar o exemplo acima em Node.js com Typescript:
import {MongoClient, Db} de'mongodb' class DBInstance { instância estática privada: Db construtor privado () {} static getInstance () { if (! this.instance) { const URL="mongodb://localhost: 27017" const dbName="amostra" MongoClient.connect (URL, (err, cliente)=> { if (err) console.log ("Erro de DB", err) const db=client.db (dbName); this.instance=db }) } return this.instance } } exportar DBInstance padrão
Aqui, temos uma classe, DBInstance
, com uma instância de atributo. Em DBInstance
, temos o método estático getInstance
onde reside nossa lógica principal.
Ele verifica se já existe uma instância do banco de dados. Se houver, ele retornará isso. Caso contrário, ele criará uma instância de banco de dados para nós e a retornará.
Aqui está um exemplo de como podemos usar o padrão singleton
em nossas rotas de API:
import express, {Application, Request, Response} de'express' importar DBInstance de'./helper/DB' importar bodyParser de'body-parser' const app=express () start de função assíncrona () { experimentar{ app.use (bodyParser.json ()) app.use (bodyParser.urlencoded ({extended: true})) const db=await DBInstance.getInstance () app.get ('/todos', async (req: Request, res: Response)=> { experimentar { const db=await DBInstance.getInstance () const todos=await db.collection ('todo'). find ({}). toArray () res.status (200).json ({sucesso: verdadeiro, dados: todos}) } pegar (e) { console.log ("Erro ao buscar", e) res.status (500).json ({sucesso: falso, dados: nulo}) } }) app.post ('/todo', assíncrono (req: Request, res: Response)=> { experimentar { const db=await DBInstance.getInstance () const todo=req.body.todos const todoCollection=await db.collection ('todo'). insertOne ({name: todo}) res.status (200).json ({sucesso: verdadeiro, dados: todoCollection}) } pegar (e) { console.log ("Erro ao inserir", e) res.status (500).json ({sucesso: falso, dados: nulo}) } }) app.listen (4000, ()=> { console.log ("Servidor está rodando na PORTA 4000") }) } pegar (e) { console.log ("Erro ao iniciar o servidor", e) } } start ()
Fábrica de abstratos
Antes de entrar na explicação da fábrica abstrata, quero que você saiba o que significa padrão de fábrica
.
Fábrica simples
Para simplificar, deixe-me fazer uma analogia. Digamos que você esteja com fome e queira um pouco de comida. Você pode cozinhar para si mesmo ou pode fazer um pedido em um restaurante. Dessa forma, você não precisa aprender ou saber cozinhar para comer um pouco.
Da mesma forma, o padrão Factory simplesmente gera uma instância de objeto para um usuário sem expor qualquer lógica de instanciação ao cliente.
Agora que sabemos sobre o padrão de fábrica simples, vamos voltar ao padrão de fábrica abstrato. Ampliando nosso exemplo de fábrica simples, digamos que você está com fome e decidiu pedir comida em um restaurante. Com base na sua preferência, você pode pedir uma cozinha diferente. Em seguida, pode ser necessário selecionar o melhor restaurante com base na culinária.
Como você pode ver, existe uma dependência entre a sua comida e o restaurante. Restaurantes diferentes são melhores para cozinhas diferentes.
Vamos implementar um padrão abstract factory
dentro de nosso aplicativo Node.js. Agora, vamos construir uma loja de laptops com diferentes tipos de computadores. Alguns dos componentes principais são Storage
e Processor
.
Vamos construir uma interface para ele:
exportar interface padrão IStorage { getStorageType (): string } importar IStorage de'./IStorage' exportar interface padrão IProcessor { attachStorage (armazenamento: IStorage): string showSpecs (): string }
A seguir, vamos implementar as interfaces de armazenamento
e processador
nas classes:
importar IProcessor de'../../Interface/IProcessor' importar IStorage de'../../Interface/IStorage' exportar a classe padrão MacbookProcessor implementa IProcessor { armazenamento: string | Indefinido MacbookProcessor () { console.log ("Macbook é construído usando chips de silício da Apple") } attachStorage (storageAttached: IStorage) { this.storage=storageAttached.getStorageType () console.log ("storageAttached", storageAttached.getStorageType ()) return this.storage +"Attached to Macbook" } showSpecs (): string { return this.toString () } toString (): string { return"AppleProcessor é criado usando Apple Silicon e"+ this.storage; } } importar IProcessor de'../../Interface/IProcessor' importar IStorage de'../../Interface/IStorage' exportar a classe padrão MacbookStorage implementa IStorage { storageSize: number construtor (storageSize: number) { this.storageSize=storageSize console.log (this.storageSize +"GB SSD é usado") } getStorageType () { retornar this.storageSize +"GB SSD" } }
Agora, vamos criar uma interface de fábrica, que tem métodos como createProcessor
e createStorage
.
import IStorage de'../Interface/IStorage' importar IProcessor de'../Interface/IProcessor' exportar interface padrão LaptopFactory { createProcessor (): IProcessor createStorage (): IStorage }
Depois que a interface de fábrica for criada, implemente-a na classe laptop. Aqui, será:
importar LaptopFactory de'../../factory/LaptopFactory' importar MacbookProcessor de'./MacbookProcessor' importar MacbookStorage de'./MacbookStorage' export class Macbook implementa LaptopFactory { storageSize: number; construtor (armazenamento: número) { this.storageSize=armazenamento } createProcessor (): any { retornar novo MacbookProcessor () } createStorage (): any { retornar novo MacbookStorage (this.storageSize) } }
Finalmente, crie uma função que chame métodos de fábrica:
importar LaptopFactory de'../factory/LaptopFactory' importar IProcessor de'../Interface/IProcessor' export const buildLaptop=(laptopFactory: LaptopFactory): IProcessor=> { const processor=laptopFactory.createProcessor () const storage=laptopFactory.createStorage () processador.attachStorage (armazenamento) processador de retorno }
Padrão Builder
O padrão de construtor permite que você crie diferentes sabores de um objeto sem usar um construtor em uma classe.
Mas por que, não podemos apenas usar um construtor?
Bem, há um problema com o construtor
em certos cenários. Digamos que você tenha um modelo User
e ele tenha atributos como:
exportar classe padrão User { firstName: string lastName: string gênero: string idade: número endereço: string país: string isAdmin: boolean construtor (firstName, lastName, address, gender, age, country, isAdmin) { this.firstName=builder.firstName this.lastName=builder.lastName this.address=builder.address this.gender=builder.gender this.age=builder.age this.country=builder.country this.isAdmin=builder.isAdmin } }
Para usar isso, você pode precisar instanciá-lo assim:
const user=new User ("","","","", 22,"", false)
Aqui, temos um argumento limitado. No entanto, será difícil manter uma vez que os atributos aumentem. Para resolver esse problema, precisamos do padrão do construtor.
Crie uma classe builder como esta:
importar usuário de'./User' exportar a classe padrão UserBuilder { firstName="" lastName="" gênero="" idade=0 endereço="" país="" isAdmin=false construtor(){ } setFirstName (firstName: string) { this.firstName=firstName } setLastName (lastName: string) { this.lastName=lastName } setGender (gender: string) { this.gender=gênero } setAge (idade: número) { this.age=age } setAddress (address: string) { this.address=endereço } setCountry (country: string) { this.country=country } setAdmin (isAdmin: boolean) { this.isAdmin=isAdmin } build (): Usuário { retornar novo usuário (este) } getAllValues () { devolva isso } }
Aqui, usamos getter
e setter
para gerenciar os atributos em nossa classe de construtor. Depois disso, use a classe builder dentro de nosso modelo:
importar UserBuilder de'./UserBuilder' exportar classe padrão User { firstName: string lastName: string gênero: string idade: número endereço: string país: string isAdmin: boolean construtor (construtor: UserBuilder) { this.firstName=builder.firstName this.lastName=builder.lastName this.address=builder.address this.gender=builder.gender this.age=builder.age this.country=builder.country this.isAdmin=builder.isAdmin } }
Adaptador
Um exemplo clássico de padrão de adaptador é uma tomada de alimentação com formato diferente. Às vezes, o soquete e o plugue do dispositivo não se encaixam. Para ter certeza de que funciona, usaremos um adaptador. Isso é exatamente o que faremos no padrão do adaptador.
É um processo de empacotar o objeto incompatível em um adaptador para torná-lo compatível com outra classe.
Até agora, vimos uma analogia para entender o padrão do adaptador. Deixe-me apresentar um caso de uso real em que um padrão de adaptador pode salvar sua vida.
Considere que temos uma classe CustomerError
:
import IError de'../interface/IError' exportar a classe padrão CustomError implementa IError { mensagem: string construtor (mensagem: string) { this.message=mensagem } serialize () { retornar esta. mensagem } }
Agora, estamos usando esta classe CustomError
em nosso aplicativo. Depois de algum tempo, precisamos mudar o método na classe por algum motivo.
Nova classe de erro personalizado
será mais ou menos assim:
exportar classe padrão NewCustomError { mensagem: string construtor (mensagem: string) { this.message=mensagem } withInfo () { return {message: this.message} } }
Nossa nova alteração travará todo o aplicativo, pois altera o método. Para resolver esse problema, o padrão do adaptador entra em ação.
Vamos criar uma classe de adaptador e resolver este problema:
import NewCustomError de'./NewCustomError' //importe CustomError de'./CustomError' exportar classe padrão ErrorAdapter { mensagem: string; construtor (mensagem: string) { this.message=mensagem } serialize () { //No futuro, substitua esta função const e=new NewCustomError (this.message).withInfo () retornar e } }
O método serialize
é o que usamos em todo o nosso aplicativo. Nosso aplicativo não precisa saber qual classe estamos usando. A classe Adapter
cuida disso para nós.
Observador
Um padrão Observer é uma forma de atualizar os dependentes quando há uma mudança de estado em outro objeto. geralmente contém Observer
e Observable
. Observer
se inscreve no Observable
e, onde houver uma mudança, o observable notifica os observadores.
Para entender esse conceito, vamos dar um caso de uso real para o padrão do observador:
Aqui, temos as entidades Author
, Tweet
e follower
. Seguidores
podem se inscrever no Autor
. Sempre que há um novo Tweet
, o seguidor
é atualizado.
Vamos implementá-lo em nosso aplicativo Node.js:
importar Tweet de"../module/Tweet"; interface padrão de exportação IObserver { onTweet (tweet: Tweet): string } importar Tweet de"../module/Tweet"; interface padrão de exportação IObservable { sendTweet (tweet: Tweet): qualquer }
Aqui, temos a interface IObservable
e IObserver
, que contém os métodos onTweet
e sendTweet
.
importar IObservable de"../interface/IObservable"; importar Tweet de"./Tweet"; importar seguidor de'./Follower' classe padrão de exportação Autor implementa IObservable { observadores protegidos: Seguidor []=[] notificar (tweet: tweet) { this.observers.forEach (observador=> { observer.onTweet (tweet) }) } inscrever-se (observador: seguidor) { this.observers.push (observador) } sendTweet (tweet: Tweet) { this.notify (tweet) } }
Follower.ts
importar IObserver de'../interface/IObserver' importar autor de'./Author' importar tweet de'./Tweet' export default class Follower implementa IObserver { nome: string construtor (nome: string) { this.name=nome } onTweet (tweet: Tweet) { console.log (this.name +"você obteve tweet=>"+ tweet.getMessage ()) return this.name +"you got tweet=>"+ tweet.getMessage () } }
E Tweet.ts
:
exportar a classe padrão Tweet { mensagem: string autor: string construtor (mensagem: string, autor: string) { this.message=mensagem this.author=autor } getMessage (): string { retornar this.message +"Tweet do Autor:"+ this.author } }
index.ts
import express, {Application, Request, Response} de'express' //importar DBInstance de'./helper/DB' importar bodyParser de'body-parser' importar seguidor de'./module/Follower' importar Autor de'./module/Author' importar tweet de'./module/Tweet' const app=express () start de função assíncrona () { experimentar{ app.use (bodyParser.json ()) app.use (bodyParser.urlencoded ({extended: true})) //const db=await DBInstance.getInstance () app.post ('/activate', async (req: Request, res: Response)=> { experimentar { const Follower1=novo seguidor ("Ganesh") const Follower2=novo seguidor ("Doe") autor const=novo autor () autor.subscribe (seguidor1) author.subscribe (seguidor2) author.sendTweet ( novo tweet ("Bem-vindo","Bruce Lee") ) res.status (200).json ({sucesso: verdadeiro, dados: nulo}) } pegar (e) { console.log (e) res.status (500).json ({sucesso: falso, dados: nulo}) } }) app.listen (4000, ()=> { console.log ("Servidor está rodando na PORTA 4000") }) } pegar (e) { console.log ("Erro ao iniciar o servidor", e) } } start ()
Padrão de estratégia
O padrão de estratégia permite que você selecione um algoritmo ou estratégia em tempo de execução. O caso de uso real para este cenário seria mudar a estratégia de armazenamento de arquivos com base no tamanho do arquivo.
Considere que você deseja gerenciar o armazenamento de arquivos com base no tamanho do arquivo em seu aplicativo:
Aqui, queremos carregar o arquivo e decidir a estratégia com base no tamanho do arquivo, que é uma condição de tempo de execução. Vamos implementar esse conceito usando um padrão de estratégia
.
Crie uma interface que precisa ser implementada por uma classe Writer
:
exportar interface padrão IFileWriter { escrever (caminho do arquivo: string | undefined): booleano }
Depois disso, crie uma classe
para lidar com arquivos se for maior:
import IFileWriter de'../interface/IFileWriter' classe padrão de exportação AWSWriterWrapper implementa IFileWriter { Escreva() { console.log ("Gravando Arquivo no AWS S3") retorno verdadeiro } }
Em seguida, crie uma classe
para lidar com arquivos menores em tamanho:
import IFileWriter de'../interface/IFileWriter' exportar a classe padrão DiskWriter implementa IFileWriter { escrever (caminho do arquivo: string) { console.log ("Gravando Arquivo no Disco", caminho do arquivo) retorno verdadeiro } }
Assim que tivermos os dois, precisamos criar o cliente que pode usar qualquer estratégia
nele:
import IFileWriter de'../interface/IFileWriter' exportar classe padrão Writer { escritor protegido construtor (escritor: IFileWriter) { this.writer=escritor } escrever (caminho do arquivo: string): boolean { retornar this.writer.write (caminho do arquivo) } }
Finalmente, podemos usar a estratégia com base na condição que temos:
let size=1000 if (tamanho <1000) { const writer=new Writer (new DiskFileWriter ()) writer.write ("o caminho do arquivo vem aqui") } outro{ const writer=new Writer (new AWSFileWriter ()) writer.write ("gravando o arquivo na nuvem") }
Cadeia de responsabilidade
A cadeia de responsabilidade permite que um objeto passe por uma cadeia de condições ou funcionalidades. Em vez de gerenciar todas as funcionalidades e condições em um só lugar, ele se divide em cadeias de condições pelas quais um objeto deve passar.
Um dos melhores exemplos desse padrão é o express middleware
:
Construímos funções como um middleware vinculado a cada solicitação do expresso. Nossa solicitação deve passar pela condição dentro do middleware. É o melhor exemplo de uma cadeia de responsabilidade
:
Fachada
O padrão de fachada nos permite envolver funções ou módulos semelhantes em uma única interface. Dessa forma, o cliente não precisa saber nada sobre como funciona internamente.
Um bom exemplo disso seria inicializar seu computador. Você não precisa saber o que acontece dentro do computador quando você o liga. Você só precisa pressionar um botão. Dessa forma, o padrão de fachada nos ajuda a fazer lógica de alto nível sem a necessidade de implementar tudo pelo cliente.
Vamos pegar um perfil de usuário como exemplo, onde temos as seguintes funcionalidades:
- Sempre que uma conta de usuário é desativada, precisamos atualizar o status e atualizar os dados bancários.
Vamos usar o padrão fachada
para implementar essa lógica em nosso aplicativo:
importar IUser de'../Interfaces/IUser' exportar classe padrão User { private firstName: string lastName privado: string private bankDetails: string | nulo idade privada: número papel privado: string private isActive: boolean construtor ({firstName, lastName, bankDetails, age, role, isActive}: IUser) { this.firstName=firstName this.lastName=lastName this.bankDetails=bankDetails this.age=age this.role=papel this.isActive=isActive } getBasicInfo () { Retorna { firstName: this.firstName, lastName: this.lastName, idade: this.age, papel: this.role } } activateUser () { this.isActive=true } updateBankDetails (bankInfo: string | null) { this.bankDetails=bankInfo } getBankDetails () { return this.bankDetails } deactivateUser () { this.isActive=false } getAllDetails () { devolva isso } } interface padrão de exportação IUser { firstName: string lastName: string bankDetails: string idade: número papel: string isActive: boolean }
Finalmente, nossa classe fachada
será:
importar usuário de'../module/User' exportar a classe padrão UserFacade { usuário protegido: usuário construtor (usuário: usuário) { this.user=usuário } activateUserAccount (bankInfo: string) { this.user.activateUser () this.user.updateBankDetails (bankInfo) retornar this.user.getAllDetails () } deactivateUserAccount () { this.user.deactivateUser () this.user.updateBankDetails (null) } }
Aqui, combinamos os métodos que precisamos chamar sempre que uma conta de usuário é desativada.
O código-fonte completo pode ser encontrado aqui .
Conclusão
Vimos apenas os padrões de design comumente usados no desenvolvimento de aplicativos. Existem muitos outros padrões de design disponíveis no desenvolvimento de software. Sinta-se à vontade para comentar seu padrão de design favorito e seu caso de uso.
A postagem Padrões de design em TypeScript e Node.js apareceu primeiro no LogRocket Blog .