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:
EmailNotificationprecisa defromSmsNotificationprecisa normalizar telefonePushNotificationprecisa dedevice_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:
- Escolhe a classe certa.
- 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.