Buscar todos los descendientes de una clase en Ruby

144

Puedo ascender fácilmente la jerarquía de clases en Ruby:

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

¿Hay alguna manera de descender la jerarquía también? Me gustaria hacer esto

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

Pero no parece haber un descendantsmétodo.

(Esta pregunta surge porque quiero encontrar todos los modelos en una aplicación Rails que descienden de una clase base y enumerarlos; tengo un controlador que puede funcionar con cualquier modelo y me gustaría poder agregar nuevos modelos sin tener que modificar el controlador.)

Ardilla Douglas
fuente

Respuestas:

146

Aquí hay un ejemplo:

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

pone Parent.descendants te da:

GrandChild
Child

pone Child.descendants te da:

GrandChild
Pedro
fuente
1
Eso funciona muy bien, gracias! Supongo que visitar cada clase puede ser demasiado lento si estás tratando de afeitarte milisegundos, pero es perfectamente rápido para mí.
Douglas Squirrel
1
singleton_classen lugar de Classhacerlo mucho más rápido (ver fuente en apidock.com/rails/Class/descendants )
brauliobo
21
Tenga cuidado si puede tener una situación en la que la clase aún no está cargada en la memoria, ObjectSpaceno la tendrá.
Edmund Lee
¿Cómo podría hacer que esto funcione Objecty BasicObject?, curiosidad por saber lo que muestran
Amol Pujari
@AmolPujari p ObjectSpace.each_object(Class)imprimirá todas las clases. También puede obtener los descendientes de cualquier clase que desee sustituyendo su nombre selfen la línea de código del método.
BobRodes
62

Si usa Rails> = 3, tiene dos opciones en su lugar. Úselo .descendantssi desea más de un nivel de profundidad de clases para niños, o úselo .subclassespara el primer nivel de clases para niños.

Ejemplo:

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
fuente
66
Tenga en cuenta que en el desarrollo, si tiene desactivada la carga ansiosa, estos métodos solo devolverán clases si se han cargado (es decir, si el servidor en ejecución ya ha hecho referencia a ellas).
stephen.hanson
1
@ stephen.hanson ¿cuál es la forma más segura de garantizar resultados correctos aquí?
Chris Edwards
26

Ruby 1.9 (o 1.8.7) con ingeniosos iteradores encadenados:

#!/usr/bin/env ruby1.9

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

Ruby pre-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

Úselo así:

#!/usr/bin/env ruby

p Animal.descendants
Jörg W Mittag
fuente
3
Esto también funciona para los módulos; simplemente reemplace ambas instancias de "Clase" con "Módulo" en el código.
korinthe
2
Para mayor seguridad, uno debe escribir ObjectSpace.each_object(::Class): esto mantendrá el código funcionando cuando tenga un YourModule :: Class definido.
Rene Saarsoo
19

Reemplazar el método de clase denominado heredado . Este método pasaría a la subclase cuando se crea, lo que puede rastrear.

Chandra Sekar
fuente
Me gusta este también. Anular el método es marginalmente intrusivo, pero hace que el método descendiente sea un poco más eficiente, ya que no tiene que visitar todas las clases.
Douglas Squirrel
@ Douglas Aunque es menos intrusivo, probablemente tendrá que experimentar para ver si satisface sus necesidades (es decir, ¿cuándo construye Rails la jerarquía del controlador / modelo?).
Josh Lee
También es más portátil para varias implementaciones de ruby ​​que no son MRI, algunas de las cuales tienen una gran sobrecarga de rendimiento por el uso de ObjectSpace. La clase # heredada es ideal para implementar patrones de "registro automático" en Ruby.
John Whitley
¿Quieres compartir un ejemplo? Dado que es a nivel de clase, supongo que tendría que almacenar cada clase en algún tipo de variable global.
Noz
@Noz No, una variable de instancia en la clase misma. Pero entonces los objetos no pueden ser recogidos por GC.
Phrogz
13

Alternativamente (actualizado para ruby ​​1.9+):

ObjectSpace.each_object(YourRootClass.singleton_class)

Forma compatible con Ruby 1.8:

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

Tenga en cuenta que esto no funcionará para los módulos. Además, YourRootClass se incluirá en la respuesta. Puede usar Array # u otra forma de eliminarlo.

apeiros
fuente
eso fue asombroso. ¿Puedes explicarme cómo funciona? Yo solíaObjectSpace.each_object(class<<MyClass;self;end) {|it| puts it}
David West
1
En ruby ​​1.8, class<<some_obj;self;enddevuelve el singleton_class de un objeto. En 1.9+ puedes usar some_obj.singleton_classen su lugar (actualicé mi respuesta para reflejar eso). Cada objeto es una instancia de su singleton_class, que también se aplica a las clases. Dado que each_object (SomeClass) devuelve todas las instancias de SomeClass, y SomeClass es una instancia de SomeClass.singleton_class, each_object (SomeClass.singleton_class) devolverá SomeClass y todas las subclases.
apeiros
10

Aunque el uso de ObjectSpace funciona, el método de clase heredado parece ser más adecuado aquí heredado (subclase) documentación de Ruby

El espacio de objetos es esencialmente una forma de acceder a cualquier cosa y a todo lo que actualmente usa memoria asignada, por lo que iterar sobre cada uno de sus elementos para verificar si es una subclase de la clase Animal no es ideal.

En el siguiente código, el método de clase Animal heredado implementa una devolución de llamada que agregará cualquier subclase recién creada a su matriz de descendientes.

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

  def self.descendants
    puts @descendants 
  end
end
Mateo Sossah
fuente
66
`@descendants || = []` de lo contrario solo obtendrás el último descendiente
Axel Tetzlaff
4

Sé que está preguntando cómo hacer esto en herencia, pero puede lograr esto directamente en Ruby espaciando el nombre de la clase ( Classo 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]

De esta manera, es mucho más rápido que compararlo con cada clase ObjectSpacecomo lo proponen otras soluciones.

Si realmente necesita esto en una herencia, puede hacer algo como esto:

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

puntos de referencia aquí: http://www.eq8.eu/blogs/13-ruby-ancestors-descendants-and-other-annoying-relatives

actualizar

al final todo lo que tienes que hacer es esto

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

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

class Foo < DarthVader
end

DarthVader.descendants #=> [Foo]

gracias @saturnflyer por su sugerencia

equivalente8
fuente
3

(Rails <= 3.0) Alternativamente, podría usar ActiveSupport :: DescendantsTracker para hacer la escritura. De la fuente:

Este módulo proporciona una implementación interna para rastrear descendientes que es más rápido que iterar a través de ObjectSpace.

Dado que está bien modularizado, puede simplemente 'seleccionar' ese módulo en particular para su aplicación Ruby.

chtrinh
fuente
2

Ruby Facets tiene descendientes de clase #,

require 'facets/class/descendants'

También es compatible con un parámetro de distancia generacional.

trans
fuente
2

Una versión simple que proporciona una matriz de todos los descendientes de una clase:

def descendants(klass)
  all_classes = klass.subclasses
  (all_classes + all_classes.map { |c| descendants(c) }.reject(&:empty?)).flatten
end
dorio
fuente
1
Esto parece una respuesta superior. Desafortunadamente, todavía cae presa de la carga perezosa de las clases. Pero creo que todos lo hacen.
Dave Morse
@DaveMorse Terminé enumerando archivos y cargando manualmente las constantes para registrarlas como descendientes (y luego terminé eliminando todo esto: D)
Dorian
1
Tenga en cuenta que #subclasseses de Rails ActiveSupport.
Alex D
1

Puedes require 'active_support/core_ext'y usar el descendantsmétodo. Mira el documento y pruébalo en IRB o en palanca. Se puede usar sin rieles.

thelostspore
fuente
1
Si tiene que agregar soporte activo a su Gemfile, entonces no es realmente "sin rieles". Simplemente elige las piezas de rieles que te gustan.
Caleb
1
Esto parece una tangente filosófica que no es relevante para el tema aquí, pero creo que usar un componente Rails necesariamente significa que uno tiene using Railsun sentido holístico.
thelostspore
0

Rails proporciona un método de subclases para cada objeto, pero no está bien documentado y no sé dónde está definido. Devuelve una matriz de nombres de clase como cadenas.

Daniel Tsadok
fuente
0

Usar descendants_tracker gem puede ayudar. El siguiente ejemplo se copia del documento de la gema:

class Foo
  extend DescendantsTracker
end

class Bar < Foo
end

Foo.descendants # => [Bar]

Esta gema es utilizada por la popular joya virtus , así que creo que es bastante sólida.

EricC
fuente
0

Este método devolverá un hash multidimensional de todos los descendientes de un Objeto.

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

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

Si tiene acceso al código antes de cargar cualquier subclase, puede usar el método heredado .

Si no lo hace (lo cual no es un caso, pero podría ser útil para cualquiera que haya encontrado esta publicación), simplemente puede escribir:

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

Lo siento si me perdí la sintaxis, pero la idea debería ser clara (no tengo acceso a Ruby en este momento).

Maciej Piechotka
fuente