githublinkedinemail
← /articles

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.

4 minruby

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:

  1. String → achou. Usa.

Quando você chama "abc".tap { }:

  1. String → não tem
  2. Comparable → não tem
  3. Object → não tem
  4. Kernel → 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
  • include ordenado errado fazendo método errado ganhar
  • prepend em runtime invalidando inline cache da VM
  • gem que faz include automá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.


Ancestors chain: como Ruby resolve métodos de verdade
Ancestors chain: como Ruby resolve métodos de verdade