Les joies de la STI

Bonjour, voici le problème que je rencontre en essayant de faire de la
STI :
J’ai une classe abstraite ‘Animal’ qui assez simple :

class Animal < ActiveRecord::Base
def foo
“bar Animal”
end
end

et une classe ‘Dog’ censée la specialiser :

class Dog < Animal
end

De plus j’ai l’association 1:1 ci-après entre un modèle ‘Man’ et mon
modèle ‘Dog’ :

class Man < ActiveRecord::Base
has_one :pet, :class_name => “Dog”
end

Enfin, pour des raisons techniques, la méthode ‘foo’ est aussi
implémentée dans la classe ‘Object’ comme ceci :

class Object
def foo
“foo Object”
end
end

(Tous les sources sont dispos à l’URL suivante : http://pastie.org/265300)
En essayant de jouer avec ce modèle j’obtiens les bizarreries
suivantes :

dog = Dog.create
=> #<Dog id: 1, man_id: nil, type: “Dog”, created_at: “2008-09-03
18:33:52”, updated_at: “2008-09-03 18:33:52”>
man = Man.create
=> #<Man id: 1, created_at: “2008-09-03 18:34:06”, updated_at:
“2008-09-03 18:34:06”>
man.pet = dog
=> #<Dog id: 1, man_id: 1, type: “Dog”, created_at: “2008-09-03
18:33:52”, updated_at: “2008-09-03 18:34:21”>
dog.foo
=> “bar Animal”
man.pet.foo
=> “foo Object”

Quelqu’un saurait-il expliquer pourquoi à travers l’association,
l’objet ‘Dog’ ne parvient pas à hériter de la méthode ‘foo’ de sa
classe parente ???

La seule manière d’obtenir le comportement souhaité est d’expliciter
ceci :

man.pet.becomes(Dog).foo
=> “bar Animal”

Merci d’avance,

Salim

J’ai tenté ça sur rails edge (2.1.0) :

class Animal < ActiveRecord::Base
belongs_to :man

def foo
“bar animal”
end
end

(comme animal belongs_to, c’est lui qui a le man_id dans sa migration)

class Dog < Animal
end

class Man < ActiveRecord::Base
has_one :pet, :class_name => ‘Dog’
end

J’ouvre une console et je fais :

m = Man.new
m.pet = Dog.new
m.foo
=> “bar Animal”

m.save
reload!
Man.find(:first).pet.foo
=> “bar Animal”

Hoho. Essayons avec la modification de “Object” :

class Object ; def foo ; “bar Object” end end
Man.find(:first).pet.foo
=> “bar Object”

Intéressant. En gros, tant que l’on ne s’amuse pas à modifier Object
l’héritage fonctionne bien comme il devrait apparemment.


Michel B.

Salim a écrit, le 09/03/2008 06:42 PM :

l’objet ‘Dog’ ne parvient pas à hériter de la méthode ‘foo’ de sa
classe parente ???

Parce que man.pet n’est pas un objet Dog, mais un AssociationProxy. Ce
dernier implémente des méthodes permettant d’éviter de créer l’objet Dog
en mémoire dans certaines situations, elles sont codées directement dans
l’AssociationProxy (c’est pourquoi sur un habtm ou has_many l’appel Ã
count fait un SELECT count(*) en base au lieu de charger tous les objets
de la relation). Si l’objet n’a pas la méthode, method_missing peut
alors (si nécessaire) créer l’objet Dog et lui passer le message.
Comme foo est dans Objet, il est également dans l’AssociationProxy…

Solution : ne pas polluer la classe Object avec foo, c’est extrèmement
fragile comme pratique. Selon ton besoin on pourra essayer de te
proposer quelque chose de plus élégant et en tout cas de robuste.

Lionel

Salim a écrit, le 09/04/2008 10:18 AM :

Merci pour ces éclaircissements. J’étais persuadé que le problème
venait de la STI mais en fait c’est bel et bien lié aux
AssociationProxy.
Ce qui n’est pas très clair, je trouve, c’est que lorsque tu fais un
peu d’introspection sur cet objet, il n’est mentionné à aucun moment
la classe AssociationProxy :

pp Man.find(:first).pet.class.ancestors

Qui te dit que la méthode class n’est pas passée à Dog ?
Et oui… c’est bien le cas (cf association_proxy.rb):

instance_methods.each { |m| undef_method m unless m =~
/(^__|^nil?$|^send$|proxy_|^object_id$)/ }
“class” ne matche pas la regex : elle est supprimée et donc c’est
l’objet masqué qui répondra.

Lionel

Merci pour ces éclaircissements. J’étais persuadé que le problème
venait de la STI mais en fait c’est bel et bien lié aux
AssociationProxy.
Ce qui n’est pas très clair, je trouve, c’est que lorsque tu fais un
peu d’introspection sur cet objet, il n’est mentionné à aucun moment
la classe AssociationProxy :

pp Man.find(:first).pet.class.ancestors
[Dog(id: integer, man_id: integer, type: string, created_at: datetime,
updated_at: datetime),
Animal(id: integer, man_id: integer, type: string, created_at:
datetime, updated_at: datetime),
ActiveRecord::Base,
ActiveRecord::AttributeMethods,
ActiveRecord::Serialization,
ActiveRecord::Calculations,
ActiveRecord::Reflection,
ActiveRecord::Transactions,
ActiveRecord::Aggregations,
ActiveRecord::Associations,
ActiveRecord::Timestamp,
ActiveRecord::Observing,
ActiveRecord::Callbacks,
ActiveRecord::Locking::Pessimistic,
ActiveRecord::Locking::Optimistic,
ActiveRecord::Validations,
Object,
PP::ObjectMixin,
InstanceExecMethods,
Base64::Deprecated,
Base64,
Kernel]

Pour info, je souhaitais à la base définir cette méthode pour tous les
AR:Base ainsi que d’autres modèles à moi, et donc, la solution de
facilité m’a poussé à définir cette méthode pour tous les Object. Du
coup, j’ai décidé d’enlever la méthode foo de la classe Object et de
la mettre uniquement là où elle est utile.

Encore une fois, merci beaucoup,

Salim

On 3 sep, 21:18, Lionel B. [email protected]

is it a bug or a feature ?

On 4 sep, 12:12, Lionel B. [email protected]

Salim a écrit, le 09/04/2008 03:01 PM :

is it a bug or a feature ?

Une fonctionnalité : tu peux traiter le proxy comme l’objet masqué la
plupart du temps ce qui simplifie beaucoup les choses.

Lionel