Procure todos os descendentes de uma classe em Ruby

144

Eu posso facilmente subir a hierarquia de classes no Ruby:

String.ancestors     # [String, Enumerable, Comparable, Object, Kernel]
Enumerable.ancestors # [Enumerable]
Comparable.ancestors # [Comparable]
Object.ancestors     # [Object, Kernel]
Kernel.ancestors     # [Kernel]

Existe alguma maneira de descer a hierarquia também? Eu gostaria de fazer isso

Animal.descendants      # [Dog, Cat, Human, ...]
Dog.descendants         # [Labrador, GreatDane, Airedale, ...]
Enumerable.descendants  # [String, Array, ...]

mas não parece haver um descendantsmétodo.

(Essa pergunta surge porque eu quero encontrar todos os modelos em um aplicativo Rails que descendem de uma classe base e listá-los; eu tenho um controlador que pode trabalhar com qualquer modelo e gostaria de poder adicionar novos modelos sem precisar modificar o controlador.)

Douglas Squirrel
fonte

Respostas:

146

Aqui está um exemplo:

class Parent
  def self.descendants
    ObjectSpace.each_object(Class).select { |klass| klass < self }
  end
end

class Child < Parent
end

class GrandChild < Child
end

puts Parent.descendants
puts Child.descendants

coloca Parent.descendants fornece:

GrandChild
Child

coloca Child.descendants fornece:

GrandChild
Petros
fonte
1
Isso funciona muito bem, obrigado! Suponho que visitar todas as aulas possa ser muito lento se você estiver tentando barbear milissegundos, mas é perfeitamente rápido para mim.
Douglas Squirrel
1
singleton_classem vez de Classtorná-lo muito mais rápido (ver fonte da apidock.com/rails/Class/descendants )
brauliobo
21
Tenha cuidado se você pode ter uma situação em que a classe ainda não está carregada na memória, ObjectSpacenão a terá.
Edmund Lee
Como eu poderia fazer este trabalho para Objecte BasicObject, curioso para saber o que elas aparecem?
Amol Pujari
O @AmolPujari p ObjectSpace.each_object(Class)imprimirá todas as classes. Você também pode obter os descendentes de qualquer classe que desejar substituindo seu nome selfna linha de código do método
precisa saber é o seguinte
62

Se você usa Rails> = 3, você tem duas opções. Use .descendantsse você desejar mais de um nível de profundidade de classes para crianças ou use .subclassespara o primeiro nível de classes para crianças.

Exemplo:

class Animal
end

class Mammal < Animal
end

class Dog < Mammal
end

class Fish < Animal
end

Animal.subclasses #=> [Mammal, Fish] 
Animal.descendants  #=> [Dog, Mammal, Fish]
dgilperez
fonte
6
Observe que, no desenvolvimento, se o carregamento ansioso estiver desativado, esses métodos retornarão as classes apenas se tiverem sido carregados (ou seja, se já tiverem sido referenciados pelo servidor em execução).
214188
1
@ stephen.hanson Qual é a maneira mais segura de garantir resultados corretos aqui?
Chris Edwards
26

Ruby 1.9 (ou 1.8.7) com iteradores encadeados bacanas:

#!/usr/bin/env ruby1.9

class Class
  def descendants
    ObjectSpace.each_object(::Class).select {|klass| klass < self }
  end
end

Ruby pré-1.8.7:

#!/usr/bin/env ruby

class Class
  def descendants
    result = []
    ObjectSpace.each_object(::Class) {|klass| result << klass if klass < self }
    result
  end
end

Use-o assim:

#!/usr/bin/env ruby

p Animal.descendants
Jörg W Mittag
fonte
3
Isso funciona para módulos também; apenas substitua as duas instâncias de "Class" por "Module" no código.
korinthe
2
Para segurança extra, deve-se escrever ObjectSpace.each_object(::Class)- isso manterá o código funcionando quando você tiver um YourModule :: Class definido.
Rene Saarsoo 03/10/19
19

Substitua o método de classe chamado herdado . Esse método receberia a subclasse quando criada, que você pode rastrear.

Chandra Sekar
fonte
Eu também gosto deste. A substituição do método é marginalmente intrusiva, mas torna o método descendente um pouco mais eficiente, pois você não precisa visitar todas as aulas.
Douglas Squirrel
@Douglas Embora seja menos invasivo, você provavelmente terá que experimentar para ver se ele atende às suas necessidades (ou seja, quando o Rails cria a hierarquia do controlador / modelo?).
Josh Lee
Também é mais portátil para várias implementações de ruby ​​que não são de ressonância magnética, algumas das quais têm uma sobrecarga de desempenho grave do uso do ObjectSpace. A classe # herdada é ideal para implementar padrões de "registro automático" no Ruby.
John Whitley.
Gostaria de compartilhar um exemplo? Já que é nível de classe, acho que você teria que armazenar cada classe em algum tipo de variável global?
Noz
@Noz Não, uma variável de instância na própria classe. Mas então os objetos não podem ser coletados pelo GC.
Phrogz
13

Como alternativa (atualizado para o ruby ​​1.9+):

ObjectSpace.each_object(YourRootClass.singleton_class)

Maneira compatível com Ruby 1.8:

ObjectSpace.each_object(class<<YourRootClass;self;end)

Observe que isso não funcionará para módulos. Além disso, YourRootClass será incluído na resposta. Você pode usar a Matriz # - ou outra maneira de removê-la.

apeiros
fonte
isso foi demais. Você pode me explicar como isso funciona? Eu useiObjectSpace.each_object(class<<MyClass;self;end) {|it| puts it}
David West
1
No ruby ​​1.8, class<<some_obj;self;endretorna a singleton_class de um objeto. Em 1.9+, você pode usar some_obj.singleton_class(atualizei minha resposta para refletir isso). Todo objeto é uma instância de sua singleton_class, que também se aplica a classes. Como each_object (SomeClass) retorna todas as instâncias de SomeClass, e SomeClass é uma instância de SomeClass.singleton_class, cada um dos objetos (SomeClass.singleton_class) retornará SomeClass e todas as subclasses.
Ap4
10

Embora o uso do ObjectSpace funcione, o método da classe herdada parece ser mais adequado aqui na documentação Ruby herdada (subclasse)

O espaço de objetos é essencialmente uma maneira de acessar tudo e qualquer coisa que esteja usando atualmente a memória alocada, portanto, a iteração sobre cada um de seus elementos para verificar se é uma sublassa da classe Animal não é o ideal.

No código abaixo, o método herdado da classe Animal implementa um retorno de chamada que adicionará qualquer subclasse recém-criada à sua matriz descendente.

class Animal
  def self.inherited(subclass)
    @descendants = []
    @descendants << subclass
  end

  def self.descendants
    puts @descendants 
  end
end
Mateo Sossah
fonte
6
`@descendants || = []` caso contrário, você só obterá o último descendente #
Axel Tetzlaff
4

Eu sei que você está perguntando como fazer isso na herança, mas você pode conseguir isso diretamente no Ruby espaçando o nome da classe ( Classou Module)

module DarthVader
  module DarkForce
  end

  BlowUpDeathStar = Class.new(StandardError)

  class Luck
  end

  class Lea
  end
end

DarthVader.constants  # => [:DarkForce, :BlowUpDeathStar, :Luck, :Lea]

DarthVader
  .constants
  .map { |class_symbol| DarthVader.const_get(class_symbol) }
  .select { |c| !c.ancestors.include?(StandardError) && c.class != Module }
  # => [DarthVader::Luck, DarthVader::Lea]

É muito mais rápido dessa maneira do que comparar com todas as classes, ObjectSpacecomo as outras soluções propostas.

Se você realmente precisa disso em uma herança, pode fazer algo assim:

class DarthVader
  def self.descendants
    DarthVader
      .constants
      .map { |class_symbol| DarthVader.const_get(class_symbol) }
  end

  class Luck < DarthVader
    # ...
  end

  class Lea < DarthVader
    # ...
  end

  def force
    'May the Force be with you'
  end
end

referências aqui: http://www.eq8.eu/blogs/13-ruby-ancestors-descendants-and-other-annoying-relatives

atualizar

no final, tudo o que você precisa fazer é isso

class DarthVader
  def self.inherited(klass)
    @descendants ||= []
    @descendants << klass
  end

  def self.descendants
    @descendants || []
  end
end

class Foo < DarthVader
end

DarthVader.descendants #=> [Foo]

obrigado @saturnflyer pela sugestão

equivalente8
fonte
3

(Rails <= 3.0) Como alternativa, você pode usar o ActiveSupport :: DescendantsTracker para executar a ação. Da fonte:

Este módulo fornece uma implementação interna para rastrear descendentes mais rapidamente do que a iteração no ObjectSpace.

Como ele é modularizado de maneira adequada, você pode simplesmente 'escolher' esse módulo específico para o seu aplicativo Ruby.

chtrinh
fonte
2

O Ruby Facets possui descendentes da classe #,

require 'facets/class/descendants'

Ele também suporta um parâmetro de distância geracional.

trans
fonte
2

Uma versão simples que fornece uma matriz de todos os descendentes de uma classe:

def descendants(klass)
  all_classes = klass.subclasses
  (all_classes + all_classes.map { |c| descendants(c) }.reject(&:empty?)).flatten
end
Dorian
fonte
1
Parece uma resposta superior. Infelizmente, ainda é vítima de carregamento lento de aulas. Mas acho que todos eles fazem.
Dave Morse
@DaveMorse acabei listar arquivos e carregar manualmente as constantes para tê-los registrado como descendentes (e, em seguida, acabou removendo essa coisa toda: D)
Dorian
1
Observe que #subclassesé do Rails ActiveSupport.
Alex D
1

Você pode require 'active_support/core_ext'e usar o descendantsmétodo Confira o documento e experimente IRB ou alavanca. Pode ser usado sem Rails.

thelostspore
fonte
1
Se você precisar adicionar suporte ativo ao seu Gemfile, não será realmente "sem trilhos". É apenas escolher as peças dos trilhos que você gosta.
Caleb
1
Isso parece uma tangente filosófica que não é relevante para o tópico aqui, mas acho que usar um componente Rails significa necessariamente que estamos using Railsem um sentido holístico.
thelostspore
0

O Rails fornece um método de subclasses para cada objeto, mas não está bem documentado e não sei onde está definido. Retorna uma matriz de nomes de classe como seqüências de caracteres.

Daniel Tsadok
fonte
0

O uso da gema descendants_tracker pode ajudar. O exemplo a seguir é copiado do documento da gema:

class Foo
  extend DescendantsTracker
end

class Bar < Foo
end

Foo.descendants # => [Bar]

Esta gema é usada pela joia virtus popular , então acho que é bastante sólida.

EricC
fonte
0

Este método retornará um hash multidimensional de todos os descendentes de um objeto.

def descendants_mapper(klass)
  klass.subclasses.reduce({}){ |memo, subclass|
    memo[subclass] = descendants_mapper(subclass); memo
  }
end

{ MasterClass => descendants_mapper(MasterClass) }
jozwright
fonte
-1

Se você tiver acesso ao código antes do carregamento de qualquer subclasse, poderá usar o método herdado .

Caso contrário (o que não é um caso, mas pode ser útil para quem encontrou este post), basta escrever:

x = {}
ObjectSpace.each_object(Class) do |klass|
     x[klass.superclass] ||= []
     x[klass.superclass].push klass
end
x[String]

Desculpe se perdi a sintaxe, mas a ideia deve estar clara (não tenho acesso ao ruby ​​no momento).

Maciej Piechotka
fonte