DEV Community

Cover image for Design Patterns: Factory Method [PT-BR]
Iaan Mesquita
Iaan Mesquita

Posted on • Updated on

Design Patterns: Factory Method [PT-BR]

English version: https://dev.to/ianito/design-patterns-factory-method-22if

Olá,

Se você ainda não me conhece, eu me chamo Iaan Mesquita.

Atuo como engenheiro de software e hoje darei início a uma série de artigos que falam um pouco sobre a área de Engenharia de Software/Arquitetura de Software.

Mas antes, você provavelmente pode se perguntar: Por que os padrões de projeto são tão importantes? Bem, eles fornecem soluções para problemas comuns com os quais nós, como desenvolvedores, precisamos lidar. Assim, o uso de padrões de projeto podem tornar o software mais fácil de entender (embora nem sempre), estender e manter.

Recomendo começar essa série por este artigo, pois irei introduzir alguns conceitos que serão fundamentais para os próximos artigos. Para evitar repetições, aconselho que você se familiarize com esses conceitos básicos.

Nota: Nem sempre seguirei as práticas mais recomendadas ao explicar um padrão de design específico (já que meu objetivo é focar no conceito em questão). Todo feedback é bem-vindo, mas entenda essa limitação pois quero explicar os conceitos de uma forma que algum iniciante possa entender. Além disso, alguns termos não serão traduzidos pois eu acho que mais bagunça do que ajuda.

Também é interessante ter uma compreensão de Programação Orientada a Objetos (POO) e dos princípios SOLID.

Resumo

Antes de começar a discutir os padrões de design, vamos dar uma olhada em alguns conceitos:

Readability vs Writability

Readability

Significa a facilidade com que um código pode ser lido e compreendido por outros desenvolvedores.

Writability

Significa a facilidade com que um código pode ser escrito. Isso não tem nada a ver com a velocidade de que você escreve um código. Podemos entender esse conceito pela facilidade de expressar nossas ideias como desenvolvedores. Se você já trabalhou com a linguagem C, pode entender esse conceito se lembrando da manipulação de strings e comparando-a com outra linguagem, tipo Javascript.

Esses dois conceitos são muito importantes no campo da engenharia de software (mas não exclusivamente). Em geral, você não criará coisas novas, mas aprimorará coisas que já existem e, para isso, a readability e writability podem afetar nosso software a longo prazo.

Se você pudesse escolher entre um software que pudesse ser lido/extendido com facilidade ou um software em que cada desenvolvedor decidisse seguir seus próprios padrões, qual você escolheria?

A menos que esteja tentando se desafiar, eu sei a resposta. xD

No entanto, isso não significa que todos os padrões comumente conhecidos ofereçam uma ótima writability ou readability. Às vezes, precisaremos priorizar um em detrimento do outro por motivos de melhor extensibilidade ou até mesmo, restrições específicas do projeto/equipe.

De qualquer forma, entender esses conceitos é crucial para discutir, decidir e aplicá-los quando necessário.

Injeção de dependência

A injeção de dependência (DI) é um padrão de design de software que define como os componentes ou objetos devem adquirir suas dependências. Em vez de um objeto criar suas próprias dependências (como instanciar dentro da implementação) ou usar instâncias globais, as dependências são "injetadas" no objeto, normalmente por meio de seu construtor, um método ou propriedades. Isso permite melhor flexibilidade para testes e manutenção no software.

Apesar de simples, discutiremos esse padrão de design no futuro.

Padrões criacionais, estruturais e comportamentais

Os padrões criacionais concentram-se em maneiras de instanciar objetos. Esses padrões oferecem várias técnicas, além do uso de construtores, para garantir a flexibilidade e a capacidade de manutenção.

Os padrões estruturais orientam a composição de objetos ou classes, visando à escalabilidade e à capacidade de manutenção.

Os padrões comportamentais tratam de como os objetos interagem e se comunicam, enfatizando as responsabilidades dos objetos e como eles se comunicam.

Porém hoje discutiremos o Método Fábrica (Factory Method) - (Padrão de Criação)

Problema

Imagine que você esteja criando um aplicativo para ajudar seu cliente a gerenciar suas contas do Instagram. Como você é uma startup, está validando suas ideias e o primeiro e único recurso que seu aplicativo tem é a capacidade de publicar no Instagram.

Mas agora seus clientes amaram seu aplicativo e estão sentindo falta de outras redes sociais, como X, Facebook, BlueSky e diversas outras, e você decidiu implementá-las.

Você ainda não sabe qual a API da rede social que irá implementar, mas sabe que é uma API.

No entanto, você sabe que cada rede social tem seu próprio método para postar nelas e também tem diferentes dependências e validações mas você sabe que o comportamento é o mesmo: postar em uma rede social.

Você concorda comigo que todos os lugares dentro do seu aplicativo que precisam ter essa funcionalidade de postar em uma rede social não precisa saber sobre a implementação? Basta funcionar como esperado e publicar nelas?

Para esse exemplo, vamos supor também que você esteja desenvolvendo seu sistema usando injeção de dependência, passando dependências pelo construtor. Um exemplo disso seria:

new TwitterAPI(messageValidator,twitterSDK);
//or and
new FacebookAPI(messageValidator,userIdHash,facebookSDK);
//or and
new InstagramAPI(instagramSDK);
Enter fullscreen mode Exit fullscreen mode

Não pense muito nos detalhes, apenas capte a ideia principal: Estamos passando algumas dependências que a classe precisa através do construtor.

Acontece que se precisarmos usar a FacebookAPI/TwitterAPI em muitos lugares, toda vez que nós quisermos usar os métodos da classe, precisaremos primeiro instanciá-la e com isso também passar suas dependências.

Mas não só isso, o que acontece se o Facebook decidir que não permitirá que você publique na plataforma deles antes de confirmar que você não é um robô e você precisa fazer essa validação por você mesmo? Como resultado, você precisará validar isso antes de enviar, e agora você tem muitos lugares para fazer essa alteração porque uma coisa mudou na forma de como se comunicar com a API do Facebook.

Portanto, uma solução para esse problema é usar Factories.

Método Fábrica (Factory Method)

O Factory Method é um padrão de design de criação que fornece uma interface para a criação de objetos, mas permite que as subclasses alterem o tipo do objeto que será criado, e os objetos retornados por um Factory Method são geralmente chamados de products.

A ideia principal do Factory Method é delegar a implementação do código de criação às suas subclasses ou classes que a implementam.

No entanto, há uma limitação: as subclasses podem retornar diferentes tipos de Product somente se esses Products tiverem uma classe ou interface base comum (APIIntegration).

Além disso, o Factory Method na classe base deve ter seu tipo de retorno (APIIntegration) declarado como essa interface.

Vamos entender alguns conceitos sobre o Factory Method usando o exemplo abaixo:

Product: É uma interface ou classe abstrata que define o tipo de objetos que o Factory Method criará.

Em nosso exemplo: Interface APIIntegration

Creator: Essa classe declara o próprio Factory Method. Esse método retorna um objeto do tipo do Product. Em algumas implementações, o Creator pode ser uma interface, uma classe abstrata ou até mesmo uma classe concreta.

Em nosso exemplo: Interface APIFactory com métodos createApi e retorna um Product do tipo APIIntegration

ConcreteCreator: Uma classe que implementa ou estende o Creator e sobrescreve o método factory para retornar uma instância de um ConcreteProduct específico.

Em nosso exemplo: TwitterAPIFactory, FacebookAPIFactory e InstagramAPIFactory

ConcreteProduct: Essa é uma implementação específica do Product. É isso que o ConcreteCreator instanciará e retornará.

Em nosso exemplo: TwitterAPI, FacebookAPI, InstagramAPI

Então, vamos dar uma olhada no código:

Não leve muito a sério os detalhes de como me conectei ao APIS ou à sintaxe, todos os exemplos são fakes e não se baseiam na documentação oficial. Apenas absorva a ideia do que estou fazendo.

As dependências das classes do ConcreteProduct são passadas por meio do construtor. (DI)

Product

interface APIIntegration {
  post(message: string): void;
}
Enter fullscreen mode Exit fullscreen mode

Creator

export interface APIFactory {
    createAPI(): APIIntegration;
  }

Enter fullscreen mode Exit fullscreen mode

ConcreteProduct

class TwitterAPI implements APIIntegration {
    // Private and readonly is the same as initializing the property through constructor method
    // this.messageValidator = messageValidator
    // and so on
  constructor(private readonly messageValidator: MessageValidator, private readonly  twitterSdk: TwitterSDK) {}
  post(message: string) {
    // A code to make it possible(Implementation)
    // Fake code below
    if (!this.messageValidator.validate(message))
      throw new Error("invalid validation");
    if (!this.twitterSdk.isConnected) throw new Error("connection refused");
    this.twitterSdk.post(message);
    console.log(`Posting on twitter: ${message}`);
  }
}
Enter fullscreen mode Exit fullscreen mode
class FacebookAPI implements APIIntegration {
    // Private and readonly is the same as initializing the property through constructor method
    // this.messageValidator = messageValidator
    // and so on
  constructor(
    private readonly messageValidator: MessageValidator,
    private readonly userIdHash: string,
    private readonly facebookSDK: FacebookSDK
  ) {} //New param (userIdHash)
  post(message: string) {
     // A code to make it possible(Implementation)
    // Fake code below
    if (!this.messageValidator.validate(message))
      throw new Error("invalid validation");
    if (!this.facebookSDK.isConnected) throw new Error("connection refused");
    if (!this.facebookSDK.findUser(userIdHash))
      throw new Error("user not found");
    this.facebookSDK.post(message);
    console.log(`Posting on facebook: ${message}`);
  }
}
Enter fullscreen mode Exit fullscreen mode
class InstagramAPI implements APIIntegration {
    // Private and readonly is the same as initializing the property through constructor method
  constructor(private readonly instagramSDK: InstagramSDK) {} //New params (userIdHash)
  post(message: string) {
    // A code to make it possible(Implementation)
    // Fake code below
    this.instagramSDK.post(message);
    console.log(`Posting on instagram: ${message}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

ConcreteCreator

class TwitterAPIFactory implements APIFactory {
  createAPI(): APIIntegration {
    const messageValidator = new MessageValidator()
    const twitterSDK = new TwitterSDK()
    return new TwitterAPI(messageValidator,twitterSDK);
  }
}
Enter fullscreen mode Exit fullscreen mode
class FacebookAPIFactory implements APIFactory {
  createAPI(): APIIntegration {
    const messageValidator = new MessageValidator()
    const facebookSDK = new FacebookSDK()
    const userIdHash = "123"
    return new FacebookAPI(messageValidator,userIdHash,facebookSDK);
  }
}
Enter fullscreen mode Exit fullscreen mode
class InstagramAPIFactory implements APIFactory {
  createAPI(): APIIntegration {
    const instagramSDK = new InstagramSDK()
    return new InstagramAPI(instagramSDK);
  }
}
Enter fullscreen mode Exit fullscreen mode

Client code

const allSocialNetworks:APIFactory[] = [new InstagramAPIFactory(),new FacebookAPIFactory(),new TwitterAPIFactory()];
for (const api of allSocialNetworks) {
    const socialNetworkAPI = api.createAPI()
    socialNetworkAPI.post('Hello Followers')
}
Enter fullscreen mode Exit fullscreen mode

Com esse padrão de design, se eu precisar modificar as dependências ou até mesmo atualizar os pacotes que o Facebook, o Twitter ou o Instagram usam, só preciso ajustar meu ConcreteCreator e/ou ConcreteProduct, desde que eu respeite meu contrato APIIntegration(Product) e APIFactory(Creator).

Qualquer parte do meu código em que eu utilize minha API permanece inalterada e não é afetada por essas mudanças. Isso isola os possíveis bugs e reduz os locais onde o código precisa ser atualizado, diminuindo as chances de bugs.

Se eu também decidir integrar outra rede social, como o Threads, é simples, basta que eu crie um ConcreteProduct e ConcreteFactory específicos. Depois, posso chamá-la no código, sem precisar fazer mudanças bruscas alterando o que já existe.

class ThreadsAPI implements APIIntegration {
  constructor(private readonly threadsSDK: ThreadsSDK) {}

  post(message: string) {
    // A code to make it possible(Implementation)
    // Fake code below
    this.threadsSDK.post(message);
    console.log(`Posting on Threads: ${message}`);
  }
}

class ThreadsAPIFactory implements APIFactory {
  createAPI(): APIIntegration {
    const threadsSDK = new ThreadsSDK();
    return new ThreadsAPI(threadsSDK);
  }
}
Enter fullscreen mode Exit fullscreen mode

E se eu também precisar criar testes para testar esse comportamento, eu não preciso usar uma implemetação real. Posso criar um ConcreteProduct e ConcreteFactory específicos que simulem esse comportamento.

Alguns pontos sobre o Factory Method

Quando falamos de design de software, não se trata apenas de fazer o código funcionar. Trata-se também de garantir que o código possa evoluir e precisamos ter em mente que tudo muda.

A tecnologia de hoje pode se tornar obsoleta amanhã. (Um salve pra galera do java escripto)

Portanto, nosso código precisa ser flexível o suficiente para acomodar essas mudanças sem ter de criar tudo novamente.

O Factory Method Pattern é bom nesses cenários:

Abstração: Ele fornece um ponto em comum com o código de diversos lugares em um só e o processo de instanciação do objeto. Essa abstração garante que, mesmo que a lógica de criação se torne complexa no futuro, ela não afetará outras partes do aplicativo.

Extensibilidade: Deseja adicionar outra rede socia ao seu aplicativo? Com o Factory Method, você pode introduzir sem esforço um novo ConcreteProduct (como uma nova integração de API) sem alterar muito do código existente.

Manutenibilidade: Como mencionado anteriormente, as alterações são localizadas. Se a API de uma determinada plataforma for alterada, só precisaremos modificar sua respectiva Factory e Product, deixando o restante do aplicativo intocado.

Esse padrão, embora não seja uma solução mágica para todos os problemas, oferece uma boa abordagem para a criação de objetos. Ele encapsula as complexidades e fornece uma estrutura clara, tornando o código mais compreensível e menos propenso a erros.

Prós e contras

Como qualquer ferramenta ou abordagem, o Factory Method tem suas vantagens e desvantagens:

Prós

  • Você evita o acoplamento forte entre a criação(creators) e a implementação(Concrete Products).

  • Single Responsability: É possível deixar a reponsabilidade de criação do Product em apenas um lugar, o que facilita a manutenção do código.

  • Open/Closed Principle: Você pode introduzir novos tipos de Products no programa sem quebrar o código do cliente existente.

Contras

  • O código pode se tornar mais complicado, pois você precisa introduzir muitas subclasses novas para implementar o padrão. O melhor cenário é quando você está introduzindo o padrão em uma hierarquia existente de classes de criadores.

  • Se a sua classe não precisar de muitas dependências, esse padrão pode ser over-engineering.

  • Se as dependência do aplicativo não mudam com frequência, o Factory Method poderá apenas aumentar a base de código sem agregar valor real. A otimização prematura é a raiz de todo o mal.

Conclusão

No software, as coisas mudam rapidamente. Portanto, precisamos de maneiras de mudar nossos aplicativos sem começar do zero.

O Factory Method nos ajuda a fazer isso. É como ter pedaços de códigos que podem ser facilmente trocados e substituídos conforme necessário.

Isso simplifica a adição de novas coisas ou a alteração de partes do nosso aplicativo.

No entanto, é importante entender os prós e os contras.

Sempre priorize a simplicidade e a clareza e use padrões onde eles realmente agregam valor.

Muito obrigado por ler até aqui :)

Sinta-se convidado a participar com dúvidas, críticas e sugestões.

Em nosso próximo artigo, discutiremos mais um pouco sobre outro tópico relacionado a engenharia de software.

Até mais :)

Referências

Top comments (2)

Collapse
 
davidfonseca profile image
David Fonseca

Muito bom o exemplo! Obrigado por compartilhar!

Collapse
 
ianito profile image
Iaan Mesquita

Obrigado. :)