Ancestors chain: como Ruby resolve métodos de verdade
Pergunta de entrevista preferida pra separar Rubyist de pessoa que escreve Ruby. Include, prepend, singleton classes — toda dúvida tem a mesma resposta.
Ancestors chain: como Ruby resolve métodos de verdade
Pergunta de entrevista preferida pra separar Rubyist de "pessoa que escreve Ruby":
"Explica a ancestors chain de cabeça."
Se você gaguejou, sem problema.
Esse artigo é pra você nunca mais gaguejar.
O que é a ancestors chain
A ancestors chain é a lista ordenada de classes e módulos que Ruby percorre para resolver um método.
Você pode ver ela:
String.ancestors
# => [String, Comparable, Object, Kernel, BasicObject]
Quando você chama "abc".upcase, Ruby procura upcase:
String→ achou. Usa.
Quando você chama "abc".tap { }:
String→ não temComparable→ não temObject→ não temKernel→ achou. Usa.
A chain é literalmente percorrida.
A regra básica de include
module M
def greet = "hi from M"
end
class A
include M
end
A.ancestors
# => [A, M, Object, Kernel, BasicObject]
include adiciona o módulo acima da classe que o incluiu, abaixo da próxima ancestor.
Ordem importa. Sempre.
prepend muda o jogo
module M
def greet = "from M, then #{super}"
end
class A
prepend M
def greet = "from A"
end
A.ancestors
# => [M, A, Object, Kernel, BasicObject]
A.new.greet
# => "from M, then from A"
prepend mete o módulo abaixo da classe — antes dela na chain.
Isso significa: o módulo intercepta a chamada antes da própria classe.
super ali chama a versão da classe.
É como Active Support faz patch em métodos sem você reescrever a classe.
Múltiplos include — ordem reversa
module A; end
module B; end
class C
include A
include B
end
C.ancestors
# => [C, B, A, Object, Kernel, BasicObject]
O último incluído fica mais perto da classe.
Faz sentido se você pensar: cada include empurra o que veio antes pra trás.
Herança + módulo
module M
def hello = "M"
end
class Parent
def hello = "Parent"
end
class Child < Parent
include M
end
Child.ancestors
# => [Child, M, Parent, Object, Kernel, BasicObject]
Child.new.hello # => "M"
M está entre Child e Parent.
Child não definiu hello → Ruby olha M → achou → usa.
Se M chamasse super, iria pra Parent#hello.
Singleton classes complicam
Cada objeto tem sua singleton class invisível.
user = User.new
def user.special = "só eu"
Onde mora special?
Na singleton class de user, que é uma camada extra antes de User.
user
↓
[singleton de user]
↓
User
↓
...
Métodos de classe (def self.foo) são singleton methods da própria classe.
Por isso class << self é a sintaxe pra abrir essa caixa.
Múltiplos prepends — ordem reversa
Igual include, mas pro outro lado:
module A; end
module B; end
class C
prepend A
prepend B
end
C.ancestors
# => [B, A, C, Object, Kernel, BasicObject]
B é o "mais externo" — intercepta primeiro.
Pensa em cebola. Cada prepend adiciona uma camada por fora.
Cenário real: Devise
Quando você adiciona devise :database_authenticatable no User, ele faz include de um módulo monstro.
Esse módulo tem valid_password?, valid_for_authentication?, etc.
Depois você customiza:
class User
devise :database_authenticatable
def valid_password?(password)
# minha lógica custom
super
end
end
Seu valid_password? está na classe.
O do Devise está no módulo.
Ancestors:
[User, DeviseModules..., ApplicationRecord, ...]
Quando você chama super, vai pra DeviseModules.
Sem entender chain, isso é mágico.
Com chain, é mecânico.
Quando a chain te morde
- monkey patch que sobrescreveu silenciosamente
includeordenado errado fazendo método errado ganharprependem runtime invalidando inline cache da VM- gem que faz
includeautomático em ActiveRecord::Base e quebra o que você não sabia que existia
Toda vez que algo "estranho" acontece com método em Ruby, a resposta está em ClasseSuspeita.ancestors.
A grande virada de chave
Ancestors chain não é trivia.
É o sistema de tipos dinâmico do Ruby em ação.
Quando você entende a chain, entende:
- como Rails é composto
- como Devise/Pundit/etc se enxertam no modelo
- por que algumas refatorações quebram tudo
- como escrever DSL sem destruir hierarquia
Conclusão
Toda dúvida sobre "por que esse método tá sendo chamado?" tem a mesma resposta:
Olha a ancestors chain.
Você pode debugar Ruby a vida inteira sem isso.
Mas vai sofrer mais do que precisa.
E quem entende a chain de cabeça é quem para de chamar Ruby de mágico — e começa a chamar de bem desenhado.
