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

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

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 new 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 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:

// Sem factory (problemático)
let user;
if (type === 'admin') {
  user = new Admin(name);
} else if (type === 'guest') {
  user = new Guest(name);
}

// Com factory (profissional)
class UserFactory {
  static create(type, name) {
    switch(type) {
      case 'admin':
        return new Admin(name, true);
      case 'premium':
        return new Premium(name, true);
      case 'guest':
        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 = ['read', 'write', 'delete'];
  }
}

class Guest {
  constructor(name, isAdmin) {
    this.name = name;
    this.isAdmin = isAdmin;
    this.permissions = ['read'];
  }
}

// Uso
const admin = UserFactory.create('admin', 'João');
const guest = UserFactory.create('guest', 'Maria');

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.

Singleton Pattern

Evitando Múltiplas Instâncias

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.

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

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('Banco não conectado');
    }
    return `Executando: ${sql}`;
  }
}

// Uso
const db1 = new Database('localhost:5432');
const db2 = new Database('other-host:5432');

console.log(db1 === db2); // true — mesma instância!
db1.connect();
console.log(db2.query('SELECT * FROM users')); // Funciona

Uma alternativa moderna e elegante é usar um closure:

const Logger = (() => {
  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('Erro crítico');
console.log(logger1 === logger2); // true

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

Builder Pattern

Construindo Objetos Complexos Passo a Passo

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.

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

class RequestBuilder {
  constructor(url) {
    this.url = url;
    this.method = 'GET';
    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('https://api.example.com/users')
  .setMethod('POST')
  .addHeader('Content-Type', 'application/json')
  .addHeader('Authorization', 'Bearer token123')
  .setBody({ name: 'João', email: 'joao@example.com' })
  .setTimeout(10000)
  .build();

console.log(request);

O padrão melhora enormemente a legibilidade. Compare: new RequestBuilder('url').setMethod('POST').addHeader(...) versus um construtor com 8 parâmetros posicionais onde você esqueceria qual vem primeiro.

Quando Usar Cada Padrão

Factory: 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.

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

Builder: 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.

Conclusão

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

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.

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.

Referências


Artigos relacionados