JavaScript Avançado

Dominando Design Patterns em JavaScript: Factory, Singleton e Builder em Projetos Reais

7 min de leitura

Dominando Design Patterns em JavaScript: Factory, Singleton e Builder em Projetos Reais

O que são Design Patterns? Design Patterns são soluções comprovadas para problemas comuns em desenvolvimento de software. Eles não são código pronto para copiar, mas sim diretrizes que orientam a estrutura e organização do seu projeto. Em JavaScript, os padrões são especialmente valiosos porque a linguagem oferece flexibilidade — talvez até demais. Dominar Factory, Singleton e Builder significa ter ferramentas para criar arquiteturas escaláveis, testáveis e maintíveis. Estes três padrões pertencem à categoria de padrões criacionais, ou seja, lidam com a forma como os objetos são instanciados. Entender quando e como usá-los é essencial para qualquer desenvolvedor que queira evoluir além de código procedural. Factory Pattern Conceito e Aplicação O Factory Pattern encapsula a lógica de criação de objetos em uma função ou classe dedicada. Em vez de espalharem por toda a aplicação, você centraliza como os objetos nascem. Isso torna mudanças futuras mais fáceis e desacopla o código que usa o objeto de sua implementação concreta. Imagine uma aplicação

<h2>O que são Design Patterns?</h2>

<p>Design Patterns são soluções comprovadas para problemas comuns em desenvolvimento de software. Eles não são código pronto para copiar, mas sim diretrizes que orientam a estrutura e organização do seu projeto. Em JavaScript, os padrões são especialmente valiosos porque a linguagem oferece flexibilidade — talvez até demais. Dominar Factory, Singleton e Builder significa ter ferramentas para criar arquiteturas escaláveis, testáveis e maintíveis.</p>

<p>Estes três padrões pertencem à categoria de padrões criacionais, ou seja, lidam com a forma como os objetos são instanciados. Entender quando e como usá-los é essencial para qualquer desenvolvedor que queira evoluir além de código procedural.</p>

<h2>Factory Pattern</h2>

<h3>Conceito e Aplicação</h3>

<p>O Factory Pattern encapsula a lógica de criação de objetos em uma função ou classe dedicada. Em vez de espalharem <code>new</code> por toda a aplicação, você centraliza como os objetos nascem. Isso torna mudanças futuras mais fáceis e desacopla o código que usa o objeto de sua implementação concreta.</p>

<p>Imagine uma aplicação com diferentes tipos de usuários: Admin, Guest e Premium. Sem Factory, seu código precisaria saber dos detalhes de cada classe. Com Factory, você delega isso:</p>

<pre><code class="language-javascript">// Sem factory (problemático)

let user;

if (type === &#039;admin&#039;) {

user = new Admin(name);

} else if (type === &#039;guest&#039;) {

user = new Guest(name);

}

// Com factory (profissional)

class UserFactory {

static create(type, name) {

switch(type) {

case &#039;admin&#039;:

return new Admin(name, true);

case &#039;premium&#039;:

return new Premium(name, true);

case &#039;guest&#039;:

return new Guest(name, false);

default:

throw new Error(Tipo ${type} desconhecido);

}

}

}

class Admin {

constructor(name, isAdmin) {

this.name = name;

this.isAdmin = isAdmin;

this.permissions = [&#039;read&#039;, &#039;write&#039;, &#039;delete&#039;];

}

}

class Guest {

constructor(name, isAdmin) {

this.name = name;

this.isAdmin = isAdmin;

this.permissions = [&#039;read&#039;];

}

}

// Uso

const admin = UserFactory.create(&#039;admin&#039;, &#039;João&#039;);

const guest = UserFactory.create(&#039;guest&#039;, &#039;Maria&#039;);</code></pre>

<p>O benefício é claro: se precisar adicionar um novo tipo de usuário, você só modifica a Factory. Toda a aplicação continua funcionando sem mudanças.</p>

<h2>Singleton Pattern</h2>

<h3>Evitando Múltiplas Instâncias</h3>

<p>Singleton garante que uma classe tenha apenas uma instância em toda a aplicação e fornece um ponto global de acesso a ela. É perfeito para objetos que representam recursos únicos: configurações, loggers, conexões de banco de dados.</p>

<p>O desafio em JavaScript é implementar isso de forma segura, impedindo que alguém acidentalmente crie uma nova instância:</p>

<pre><code class="language-javascript">class Database {

constructor(connectionString) {

// Previne múltiplas instâncias

if (Database.instance) {

return Database.instance;

}

this.connectionString = connectionString;

this.connected = false;

Database.instance = this;

}

connect() {

if (!this.connected) {

console.log(Conectando a ${this.connectionString});

this.connected = true;

}

}

query(sql) {

if (!this.connected) {

throw new Error(&#039;Banco não conectado&#039;);

}

return Executando: ${sql};

}

}

// Uso

const db1 = new Database(&#039;localhost:5432&#039;);

const db2 = new Database(&#039;other-host:5432&#039;);

console.log(db1 === db2); // true — mesma instância!

db1.connect();

console.log(db2.query(&#039;SELECT * FROM users&#039;)); // Funciona</code></pre>

<p>Uma alternativa moderna e elegante é usar um closure:</p>

<pre><code class="language-javascript">const Logger = (() =&gt; {

let instance;

return {

getInstance() {

if (!instance) {

instance = {

logs: [],

log(message) {

this.logs.push(message);

console.log(message);

}

};

}

return instance;

}

};

})();

const logger1 = Logger.getInstance();

const logger2 = Logger.getInstance();

logger1.log(&#039;Erro crítico&#039;);

console.log(logger1 === logger2); // true</code></pre>

<p>Use Singleton com moderação — dependências globais podem complicar testes unitários. Prefira injeção de dependência quando possível.</p>

<h2>Builder Pattern</h2>

<h3>Construindo Objetos Complexos Passo a Passo</h3>

<p>O Builder Pattern é ideal quando você precisa criar objetos com muitos parâmetros opcionais ou configurações complexas. Em vez de um construtor gigante, você encadeia métodos de configuração, tornando o código legível e flexível.</p>

<p>Considere montar uma requisição HTTP com várias opções:</p>

<pre><code class="language-javascript">class RequestBuilder {

constructor(url) {

this.url = url;

this.method = &#039;GET&#039;;

this.headers = {};

this.body = null;

this.timeout = 5000;

}

setMethod(method) {

this.method = method;

return this; // Retorna this para encadeamento

}

addHeader(key, value) {

this.headers[key] = value;

return this;

}

setBody(body) {

this.body = body;

return this;

}

setTimeout(ms) {

this.timeout = ms;

return this;

}

build() {

return {

url: this.url,

method: this.method,

headers: this.headers,

body: this.body,

timeout: this.timeout

};

}

}

// Uso fluido e intuitivo

const request = new RequestBuilder(&#039;https://api.example.com/users&#039;)

.setMethod(&#039;POST&#039;)

.addHeader(&#039;Content-Type&#039;, &#039;application/json&#039;)

.addHeader(&#039;Authorization&#039;, &#039;Bearer token123&#039;)

.setBody({ name: &#039;João&#039;, email: &#039;joao@example.com&#039; })

.setTimeout(10000)

.build();

console.log(request);</code></pre>

<p>O padrão melhora enormemente a legibilidade. Compare: <code>new RequestBuilder(&#039;url&#039;).setMethod(&#039;POST&#039;).addHeader(...)</code> versus um construtor com 8 parâmetros posicionais onde você esqueceria qual vem primeiro.</p>

<h2>Quando Usar Cada Padrão</h2>

<p><strong>Factory</strong>: Use quando você precisa criar objetos de diferentes tipos baseado em condições. Exemplos reais: loaders de arquivo (ImageFactory, VideoFactory), criadores de widgets UI, geradores de reportes.</p>

<p><strong>Singleton</strong>: Use para recursos únicos que devem ser acessados globalmente. Exemplos: logger único, pool de conexões, configurações da aplicação, cache central.</p>

<p><strong>Builder</strong>: Use quando o objeto tem muitos parâmetros opcionais ou quando a construção é complexa. Exemplos: configuradores de aplicação, construtores de queries SQL, builders de componentes UI.</p>

<h2>Conclusão</h2>

<p>Design Patterns não são sobre memorizar nomes — são sobre reconhecer problemas e aplicar soluções comprovadas. <strong>Factory resolve o problema de criação variável</strong>, encapsulando lógica condicional. <strong>Singleton garante unicidade</strong>, útil para recursos compartilhados. <strong>Builder torna a construção legível e flexível</strong>, especialmente com muitas opções.</p>

<p>A chave é moderação: use padrões quando agregam valor real, não por usar. Um Singleton desnecessário complica testes. Uma Factory trivial é overhead. Código simples que funciona vence padrões mal aplicados.</p>

<p>Continue praticando, estude padrões estruturais (Decorator, Adapter) e comportamentais (Observer, Strategy) depois. A jornada é gradual, mas cada padrão dominado torna você um desenvolvedor mais capaz.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment" target="_blank" rel="noopener noreferrer">MDN Web Docs - Destructuring Assignment</a></li>

<li><a href="https://javascript.info/design-patterns" target="_blank" rel="noopener noreferrer">JavaScript.info - Design Patterns</a></li>

<li><a href="https://refactoring.guru/design-patterns" target="_blank" rel="noopener noreferrer">Refactoring.guru - Design Patterns</a></li>

<li><a href="https://github.com/getify/You-Dont-Know-JS" target="_blank" rel="noopener noreferrer">You Don&#039;t Know JS - Scope &amp; Closures</a></li>

<li><a href="https://en.wikipedia.org/wiki/Design_Patterns" target="_blank" rel="noopener noreferrer">Gang of Four - Design Patterns (Livro Clássico)</a></li>

</ul>

Comentários

Mais em JavaScript Avançado

Dominando React Internals: Reconciler, Fiber Architecture e Rendering Phases em Projetos Reais
Dominando React Internals: Reconciler, Fiber Architecture e Rendering Phases em Projetos Reais

Entendendo o Reconciler: O Coração do React O Reconciler é o mecanismo que Re...

NestJS com TypeScript: Arquitetura Modular, DI e Guards na Prática
NestJS com TypeScript: Arquitetura Modular, DI e Guards na Prática

Arquitetura Modular no NestJS O NestJS foi construído com a modularidade como...

O que Todo Dev Deve Saber sobre Contract Testing com Pact: Garantindo Compatibilidade entre Serviços
O que Todo Dev Deve Saber sobre Contract Testing com Pact: Garantindo Compatibilidade entre Serviços

O Que é Contract Testing com Pact? Contract Testing é uma metodologia que val...