Ruby do Zero: Métodos de Classe, self e Colaboração entre Objetos

Capítulo 18

Tempo estimado de leitura: 9 minutos

+ Exercício

self em Ruby: o que é e por que importa

Em Ruby, self é uma referência ao “objeto atual” no ponto exato em que o código está sendo executado. Isso muda conforme o contexto (top-level, dentro de uma instância, dentro da classe, dentro de um módulo). Entender self ajuda a diferenciar métodos de instância e métodos de classe, além de evitar confusões com variáveis e chamadas de método.

Como inspecionar self rapidamente

Uma forma prática de entender o contexto é imprimir self e sua classe:

puts self
puts self.class

Em um arquivo Ruby executado diretamente, no topo do arquivo, self costuma ser o objeto especial main (uma instância de Object).

self em diferentes contextos

1) No topo do arquivo (top-level)

# arquivo: demo_self.rb
puts self        # => main
puts self.class  # => Object

Nesse contexto, quando você define um método “solto” (fora de classes), ele vira um método privado de Object, disponível em todo lugar (o que costuma ser indesejado em projetos). Prefira organizar em classes/módulos.

2) Dentro de um método de instância

Dentro de um método de instância, self é a instância que recebeu a chamada.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

class User
  def initialize(name)
    @name = name
  end

  def greet
    puts self        # => #<User:...>
    puts self.class  # => User
    "Olá, #{@name}"
  end
end

u = User.new("Ana")
puts u.greet

3) Dentro do corpo da classe (fora de métodos)

No corpo da classe, self é o próprio objeto-classe. Isso é útil para definir métodos de classe e configurações.

class User
  puts self       # => User
  puts self.class # => Class
end

4) Dentro de um método de classe

Em um método de classe, self também é o objeto-classe (por exemplo, User).

class User
  def self.role
    puts self       # => User
    "member"
  end
end

puts User.role

5) Dentro de módulos e namespaces

Em módulos, self é o módulo (no corpo) e pode ser usado para definir métodos de módulo (métodos “estáticos” do módulo).

module Slug
  def self.normalize(text)
    text.downcase.strip.gsub(" ", "-")
  end
end

puts Slug.normalize("Olá Mundo")

Métodos de instância vs métodos de classe

Quando usar método de instância

Use método de instância quando o comportamento depende do estado de um objeto específico (variáveis de instância como @name, @items, etc.).

class Cart
  def initialize
    @items = []
  end

  def add(item)
    @items << item
  end
end

Quando usar método de classe

Use método de classe quando o comportamento pertence ao “tipo” e não a uma instância específica: fábricas (factory methods), construtores alternativos, utilitários relacionados ao domínio, ou consultas que retornam instâncias.

class Money
  def initialize(cents)
    @cents = cents
  end

  def self.from_reais(value)
    cents = (value * 100).round
    new(cents)
  end
end

m = Money.from_reais(10.50)

Dois jeitos comuns de definir métodos de classe

Ambos são equivalentes; escolha um padrão e mantenha consistência.

class User
  def self.build_guest
    new("Guest")
  end
end
class User
  class << self
    def build_guest
      new("Guest")
    end
  end
end

Por que às vezes você precisa escrever self. explicitamente

Ruby permite chamar métodos sem receptor explícito (parece uma “função”), e isso pode confundir atribuição com chamada de método setter. Para chamar um setter, você geralmente precisa de self..

Exemplo clássico: setter vs variável local

class Profile
  attr_accessor :name

  def initialize(name)
    # Sem self, Ruby interpreta como variável local 'name = ...'
    self.name = name
  end
end

Regra prática: dentro da instância, use self.algo = ... quando estiver chamando um setter.

Padrões comuns com métodos de classe

Factory method (método fábrica)

Uma fábrica encapsula regras para criar objetos. Ajuda a evitar initialize com muitos parâmetros e condicionais.

class User
  def initialize(name, role)
    @name = name
    @role = role
  end

  def self.guest
    new("Guest", :guest)
  end

  def self.admin(name)
    new(name, :admin)
  end
end

u1 = User.guest
u2 = User.admin("Carla")

Construtores alternativos (parse, from_*, build_*)

Construtores alternativos criam instâncias a partir de formatos diferentes (string, hash, etc.).

class Point
  def initialize(x, y)
    @x = x
    @y = y
  end

  def self.parse(text)
    x_str, y_str = text.split(",")
    new(Integer(x_str), Integer(y_str))
  end
end

p1 = Point.parse("10,20")

Validação e normalização na criação

Uma fábrica pode normalizar dados e validar antes de instanciar.

class Email
  def initialize(value)
    @value = value
  end

  def self.build(raw)
    value = raw.to_s.strip.downcase
    raise ArgumentError, "email inválido" unless value.include?("@")
    new(value)
  end
end

email = Email.build("  TESTE@EXEMPLO.COM ")

Colaboração entre objetos: evitando classes inchadas

Um erro comum é colocar responsabilidades demais em uma única classe: validação, cálculo, formatação, persistência, envio de e-mail, etc. Em Ruby, é natural criar objetos pequenos que colaboram: um objeto usa outro para cumprir uma tarefa.

Exemplo de classe inchada (problema)

Imagine um Order que calcula total, aplica desconto, formata recibo e ainda envia e-mail:

class Order
  def initialize(items, customer_email)
    @items = items
    @customer_email = customer_email
  end

  def total
    @items.sum { |i| i[:price] * i[:qty] }
  end

  def total_with_discount(coupon)
    t = total
    if coupon == "OFF10"
      t * 0.9
    else
      t
    end
  end

  def receipt_text
    lines = @items.map { |i| "#{i[:qty]}x #{i[:name]}" }
    lines.join("\n") + "\nTotal: #{total}"
  end

  def email_receipt
    # ... envio de email ...
  end
end

Problemas: difícil testar partes isoladas, difícil reaproveitar regras (desconto/recibo), e qualquer mudança aumenta o risco de quebrar algo.

Refatoração 1: separar cálculo de total (objeto colaborador)

Passo a passo:

  • Extrair a regra de total para uma classe dedicada.
  • Fazer Order delegar o cálculo.
  • Testar manualmente rodando um script simples.
class OrderTotalCalculator
  def initialize(items)
    @items = items
  end

  def total
    @items.sum { |i| i[:price] * i[:qty] }
  end
end

class Order
  def initialize(items, customer_email)
    @items = items
    @customer_email = customer_email
  end

  def total
    OrderTotalCalculator.new(@items).total
  end
end

Script de teste manual

# arquivo: test_total.rb
require_relative "order"

items = [
  { name: "Teclado", price: 100.0, qty: 2 },
  { name: "Mouse", price: 50.0, qty: 1 }
]

order = Order.new(items, "a@b.com")
puts order.total  # esperado: 250.0

Ao rodar o script, você valida a regra sem depender de outras partes (recibo, desconto, e-mail).

Refatoração 2: desconto como estratégia (colaboração e extensibilidade)

Em vez de condicional por cupom dentro de Order, crie objetos de desconto. Isso reduz if/else e facilita adicionar novos cupons.

class NoDiscount
  def apply(amount)
    amount
  end
end

class PercentageDiscount
  def initialize(percent)
    @percent = percent
  end

  def apply(amount)
    amount * (1.0 - @percent)
  end
end

class CouponFactory
  def self.build(code)
    case code
    when "OFF10" then PercentageDiscount.new(0.10)
    when nil, "" then NoDiscount.new
    else
      raise ArgumentError, "cupom desconhecido"
    end
  end
end

class Order
  def initialize(items, customer_email)
    @items = items
    @customer_email = customer_email
  end

  def total
    OrderTotalCalculator.new(@items).total
  end

  def total_with_coupon(code)
    discount = CouponFactory.build(code)
    discount.apply(total)
  end
end

Teste manual via script

# arquivo: test_discount.rb
require_relative "order"

items = [
  { name: "Teclado", price: 100.0, qty: 2 },
  { name: "Mouse", price: 50.0, qty: 1 }
]

order = Order.new(items, "a@b.com")
puts order.total_with_coupon("OFF10") # esperado: 225.0
puts order.total_with_coupon(nil)     # esperado: 250.0

Note como Order não precisa conhecer detalhes de cada cupom; ela apenas colabora com um objeto que responde a apply.

Refatoração 3: recibo como objeto de apresentação (separar formatação)

Formatação de texto é uma responsabilidade diferente de regras de negócio. Extraia para um gerador de recibo.

class ReceiptBuilder
  def initialize(items, total)
    @items = items
    @total = total
  end

  def text
    lines = @items.map { |i| "#{i[:qty]}x #{i[:name]} - #{format("%.2f", i[:price])}" }
    (lines + ["Total: #{format("%.2f", @total)}"]).join("\n")
  end
end

class Order
  def initialize(items, customer_email)
    @items = items
    @customer_email = customer_email
  end

  def total
    OrderTotalCalculator.new(@items).total
  end

  def receipt_text
    ReceiptBuilder.new(@items, total).text
  end
end

Teste manual via script

# arquivo: test_receipt.rb
require_relative "order"

items = [
  { name: "Teclado", price: 100.0, qty: 2 },
  { name: "Mouse", price: 50.0, qty: 1 }
]

order = Order.new(items, "a@b.com")
puts order.receipt_text

Agora você pode ajustar o layout do recibo sem tocar em cálculo de total ou descontos.

self e colaboração: onde entram métodos de classe

Métodos de classe são úteis para construir colaboradores e manter a criação centralizada. No exemplo, CouponFactory.build é um método de classe que retorna um objeto de desconto apropriado. Esse padrão é comum quando:

  • Você quer esconder detalhes de criação (quais classes concretas existem).
  • Você quer normalizar/validar entradas antes de criar.
  • Você quer manter initialize simples e previsível.

Comparando: criar direto vs usar fábrica

AbordagemComo ficaImpacto
Criar diretoPercentageDiscount.new(0.10)Espalha regras de criação pelo código
FábricaCouponFactory.build(code)Centraliza regras e facilita manutenção

Organizando responsabilidades para melhorar testabilidade manual

Ao separar em objetos pequenos, você consegue testar manualmente cada peça com scripts curtos, sem precisar “montar o sistema inteiro”. Um checklist prático:

  • Regra de cálculo: teste OrderTotalCalculator com diferentes itens.
  • Desconto: teste cada classe de desconto isoladamente e a fábrica com códigos válidos/ inválidos.
  • Apresentação: teste ReceiptBuilder com itens e total conhecido.
  • Integração leve: teste Order chamando total, total_with_coupon e receipt_text.

Exemplo: script único de verificação rápida

# arquivo: smoke_test.rb
require_relative "order"

items = [
  { name: "Teclado", price: 100.0, qty: 2 },
  { name: "Mouse", price: 50.0, qty: 1 }
]

order = Order.new(items, "a@b.com")

puts "Total: #{order.total}"
puts "Total OFF10: #{order.total_with_coupon("OFF10")}" 
puts "--- RECIBO ---"
puts order.receipt_text

Armadilhas comuns e como evitar

Confundir método de classe com método de instância

Se você definir def self.build, você chama com Classe.build. Se definir def build, você chama em uma instância: obj.build. Se errar, verá erros como NoMethodError.

class A
  def self.x; end
  def y; end
end

A.x      # ok
A.y      # erro: y é de instância
A.new.y  # ok

Usar self sem necessidade e reduzir legibilidade

Dentro de métodos de instância, não é necessário escrever self para chamar métodos comuns (sem setter). Use self quando:

  • Você precisa chamar um setter (self.name = ...).
  • Você quer deixar explícito o receptor por clareza em um ponto crítico.

Classes “inchadas” por falta de colaboradores

Se uma classe começa a ter muitos métodos com temas diferentes (cálculo, formatação, IO, validação, integração), extraia colaboradores. Uma dica prática: se você consegue nomear um grupo de métodos com um substantivo (ex.: “calculadora”, “formatador”, “validador”), provavelmente merece uma classe própria.

Agora responda o exercício sobre o conteúdo:

Em Ruby, em qual situação faz sentido usar um método de classe em vez de um método de instância?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Métodos de classe são adequados quando a lógica pertence à classe (ao tipo) e centraliza a criação/normalização, como fábricas e construtores alternativos. Se depende do estado de um objeto, o mais apropriado é um método de instância.

Próximo capitúlo

Ruby do Zero: Tratamento de Exceções — begin/rescue/ensure e Erros Comuns

Arrow Right Icon
Capa do Ebook gratuito Ruby do Zero: Fundamentos, Coleções, Blocos e Organização de Código
86%

Ruby do Zero: Fundamentos, Coleções, Blocos e Organização de Código

Novo curso

21 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.