Le Blog du Geek Joyeux

Plus moins vite tu codes, moins plus vite ça plante

Connaitre les classes heritant d'une autre classe en Ruby

| Commentaires

En Ruby, pour connaître la classe parente d’une classe, un appel de méthode suffit. Malheureusement, il n’existe pas de telle méthode afin d’obtenir les classes héritant d’une classe donnée. Voici donc comment créer ce comportement.

Prérequis: une connaissance basique du langage Ruby est nécessaire pour comprendre certains listings de code.

Petit rappel

Pour connaître la classe parente d’une classe en Ruby il suffit de faire :

1
2
3
4
5
class MaClasse
end

MaClasse.superclass
# => Object

Pour connaître les ancêtres d’une classe il suffit de faire :

1
2
MaClasse.ancestors
# => [MaClasse, Object, Kernel]

Définition d’une méthode de classe

Ce que nous cherchons à obtenir est une méthode de classe, le but étant de faire ceci :

1
2
3
4
5
6
7
8
9
10
11
class ClasseA
end

ClasseA.children
# => []

class ClasseB < ClasseA
end

ClasseA.children
# => [ClasseB]

Pour définir une méthode de classe il y à deux façons (équivalentes) :

1
2
3
4
5
6
7
8
9
class MaClasse
  def self.meth1
  end

  class << self
    def meth2
    end
  end
end

La deuxième façon peut sembler un peu obscure même pour quelqu’un ayant de bonnes bases de Ruby… Je vais donc l’expliquer en détail.

En Ruby, TOUT est objet. Cela implique qu’une classe elle-même est un objet et donc… une instance de classe : la classe Class (oui c’est recherché… mais ça a le mérite d’être explicite).

Cette “instance” n’aurait aucun sens en plusieurs exemplaires, c’est pourquoi c’est un singleton.

De par l’architecture même de Ruby, il est possible d’éditer la définition de ce singleton. C’est ce que l’on fait lorsque l’on rajoute une méthode de classe.

Écrire class << self revient donc à entrer en mode d’édition de ce singleton.

La méthode inherited(by)

Comme dit précédemment, une classe est une instance singleton de la classe Class. Lorsque l’on regarde la documentation de la classe Class, on constate l’existence d’une méthode inherited. La lecture de sa documentation nous informe que c’est une méthode de callback appelée à chaque fois qu’une classe enfant de la classe courante est créée.

Voila qui est intéressant !!

Écriture de notre méthode de classe

Grâce à la méthode inherited, nous allons pouvoir écrire notre méthode de classe ; de la manière la plus simple qui soit puisque nous allons redéfinir cette méthode.

1
2
3
4
5
6
7
8
9
10
11
12
class MaClasse
  class << self
    def inherited(by)
      puts "La classe #{by} hérite de #{self.name}"
    end
  end
end


class Enfant < MaClasse
end
# => La classe Enfant hérite de MaClasse

Effet de bord sympathique, la méthode inherited… est héritée par la classe Enfant :

1
2
3
class PetitEnfant < Enfant
end
# => La classe PetitEnfant hérite de Enfant

Bon c’est bien beau tout ça… mais en attendant, on n’a toujours pas la liste des enfants de notre classe ! J’y viens…

Maintenant que l’on sait utiliser inherited, le reste du travail est assez trivial.
En voici le code :

1
2
3
4
5
6
7
8
9
10
11
12
class MaClasse
  @children = []

  class << self
    attr_reader :children

    def inherited(by)
      @children << by
      by.instance_variable_set(:@children, [])
    end
  end
end

Bon, ça, ça marche. Mais pourquoi ça marche ?
Explication :

1
@children = []

Ici, on crée une variable d’instance, un tableau vide qui contiendra par la suite les classes enfants. Cette variable d’instance appartient à la classe elle-même (l’instance de Class).
Placer cette variable dans class << self n’aurait aucun sens…

1
attr_reader :children

De la même manière que pour une classe normale, on donne accès en lecture à la variable d’instance @children.

1
@children << by

La classe passée en paramètre à inherited, la classe héritant de notre classe donc, est insérée dans le tableau @children.

1
by.instance_variable_set(:@children, [])

Cette ligne fait en sorte que la classe enfant contienne une variable @children déjà initialisée à []. Ceci évite de devoir réécrire la ligne @children = [] à chaque fois.

Aller plus loin…

Généraliser ce fonctionnement

Étant donné qu’il est possible d’éditer n’importe quelle classe en Ruby, pourquoi ne pas généraliser notre fonctionnement à toutes les classes ?

Ceci se fait très simplement en éditant la classe la plus haut placée, j’ai nommé Object :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Object
  @children = []

  class << self
    attr_reader :children

    def inherited(by)
      @children << by
      by.instance_variable_set(:@children, [])
    end
  end
end

class MaClasse
end

Object.children
# => [MaClasse]

class Enfant < MaClasse
end

MaClasse.children
# => [Enfant]

Découvrir toute la hiérarchie

Pourquoi ne pas pousser le bouchon encore plus loin Maurice ? Et si on faisait en sorte de récupérer toute la descendance d’une classe ?
C’est parti !

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class MaClasse
    @children = []

    class << self
        attr_reader :children

        def inherited(by)
            @children << by
            by.instance_variable_set(:@children, [])
        end

        def all_children
            result = {}
            @children.each do |child|
                result[child] = child.all_children
            end
            result.empty? ? nil : result
        end
    end
end

class Enfant < MaClasse
end

class PetitEnfant < Enfant
end

class AutreClasse < MaClasse
end

MaClasse.all_children
# => {Enfant=>{PetitEnfant=>nil}, AutreClasse=>nil}

Déjà !? Et oui… c’est aussi simple que ça… :)

Commentaires