Na prática - Factory em Ruby

Se você quer criar objetos sem espalhar if/else, case, new e regras por todo o código criando um verdadeiro spaghet. Ruby é uma linguagem muito simples e falta de disciplina pode levar facilmente para essa bagunça. Então design pattern factory pode ser uma boa para evitar isso.

O problema

Imagine um funcionalidade para envio de notificações por diferentes canais: e-mail, SMS, push e etc.

class Notifier
  def initialize(channel)
    @channel = channel
  end

  def deliver(to:, message:)
    case @channel
    when :email
      EmailNotification.new(to, message).deliver
    when :sms
      SmsNotification.new(to, message).deliver
    when :push
      PushNotification.new(to, message).deliver
    else
      raise "Canal inválido: #{@channel}"
    end
  end
end

O que você achou? Parece certo? Vamos ver:

  • EmailNotification precisa de from
  • SmsNotification precisa normalizar telefone
  • PushNotification precisa de device_token
  • Amanhã entra WhatsApp
  • Depois entra Slack
  • E do nada logs e métricas

O resultado disso é que vamos ter que colocar todas a regras de construção dentro da classe que instancia esses objetos. Muito ruim, não é?

A ideia do Factory

Direto ao ponto:

  1. Escolhe a classe certa.
  2. Cria o objeto com os detalhes certos.

Quem usa o serviço não deve entender nada disso. Apenas que responsa deliver.

Vamos ao código

1) Defina uma interface (contrato).

Ruby não tem a ideia pura do que é uma interface, então podemos simular isso com herança e causando um exection quando o método não é criado.

class Notification
  def deliver
    raise NotImplementedError
  end
end

2) Implementação

class EmailNotification < Notification
  def initialize(to:, message:, from:)
    @to = to
    @message = message
    @from = from
  end

  def deliver
    puts "[EMAIL] From: #{@from} -> #{@to}: #{@message}"
  end
end

class SmsNotification < Notification
  def initialize(to:, message:)
    @to = normalize_phone(to)
    @message = message
  end

  def deliver
    puts "[SMS] -> #{@to}: #{@message}"
  end

  private

  def normalize_phone(phone)
    phone.to_s.gsub(/\D/, "")
  end
end

class PushNotification < Notification
  def initialize(device_token:, message:)
    @device_token = device_token
    @message = message
  end

  def deliver
    puts "[PUSH] token=#{@device_token}: #{@message}"
  end
end

3) A Factory

E aqui está ela:

class NotificationFactory
  def self.build(channel, **payload)
    case channel.to_sym
    when :email
      EmailNotification.new(
        to: payload.fetch(:to),
        message: payload.fetch(:message),
        from: payload.fetch(:from, "no-reply@exemplo.com")
      )
    when :sms
      SmsNotification.new(
        to: payload.fetch(:to),
        message: payload.fetch(:message)
      )
    when :push
      PushNotification.new(
        device_token: payload.fetch(:device_token),
        message: payload.fetch(:message)
      )
    else
      raise ArgumentError, "Canal inválido: #{channel}"
    end
  end
end

4) Serviço que usa a Factory

Sem saber de nada do que acontece lá dentro. Utilizando apenas um symbol para identificar o canal.

class DeliverNotification
  def self.call(channel:, **payload)
    notification = NotificationFactory.build(channel, **payload)
    notification.deliver
  end
end

DeliverNotification.call(
  channel: :email,
  to: "cliente@exemplo.com",
  message: "Seu pedido foi aprovado!",
  from: "suporte@exemplo.com"
)

DeliverNotification.call(
  channel: :sms,
  to: "(51) 99999-1234",
  message: "Seu código é 123456"
)

DeliverNotification.call(
  channel: :push,
  device_token: "abc123",
  message: "Você tem uma nova mensagem"
)

Fechando

Factory é um jeito direto de parar de espalhar decisão de instância e regra de construção pelo projeto.
Se você tem:

  • Vários tipos (classes) para fazer a mesma coisa.
  • Escolha baseada em config, input, feature flag, plano do cliente, etc.
  • Necessidade de trocar implementações com o mínimo de dor.

É a forma mais simples de implementar sem encher o código de cases, if, elses e etc.