What is the best practice to get the same behavior from multiple AR objects that are referred to polymorphically?
This is a Rails开发者_开发知识库 3 application.
I have images that can be tied to either a Product or a Brand. A product has an identifier and a Brand has a name.
The polymorphic relationship from Image is called "linkable".
If I want to list the items that a particular image is linked to, I want to avoid doing a conditional in the view like this:
<% for image in Image.all %>
<% if image.linkable.class.name=="Product" %>
<%= image.linkable.identifier %>
<% elsif image.linkable.class.name=="Brand" %>
<%= image.linkable.name %>
<% end %>
<% end %>
Of course I could just put a method inside Brand called "identifier" and use it to call "name". But that's not extensible if i want to add more objects that an image can be linked to. 8 years ago in Java programming I could mandate that a class implemented an Interface, but I don't know if I can do anything like that in Ruby. I appreciate any guidance anybody can offer.
While I like the answer you accepted, let me rewrite your example to be a bit more readable using more idiomatic ruby:
<% Image.each do |image| %>
<%= case image.linkable.class.name
when "Product"
image.linkable.identifier
when "Brand"
image.linkable.name
end %>
<% end %>
You could also easily extract that case statement into a helper function, which might be a good in-between solution if you don't want to create the module and extend it.
in a helper file:
def link_name(image)
case image.linkable.class.name
when "Product"
image.linkable.identifier
when "Brand"
image.linkable.name
end
end
and then your views become:
<% Image.each do |image| %>
<%= link_name(image) %>
<% end %>
You could create a module called Linkable and create the behavior methods in that. Then you extend the module in the classes where you want to add those behaviors. This way you don't have to worry about inheriting from anything you can just mix-in the behavior.
This is the standard Ruby way of adding common functionality to multiple classes without inheriting. You would also, by convention, name your module using a verb based adjective instead of a verb; Linkable vs. Link.
For instance:
module Linkable
def link
puts "Look, I'm linked!"
end
end
class Product < ActiveRecord
extend Linkable
end
class Brand < ActiveRecord
extend Linkable
end
Of course your classes and the module will have actual functionality.
I did it with plain sql
Image.find_by_sql("SELECT * from images INNER JOIN products on (images.linkable_id = products.id AND images.linkable_type = "product");")
Adding the method inside Brand
(or Product
) is a good way. Since this identifier method represents the contract to the object that an image can be linked to. You can unify it for all, say image_identifier
and add this method to all the classes that image links to.
Of course, adding the method to Brand
does not only mean defining it inside the class. It can be (rather should) done through a module that is extended by the linkables.
Here is how I tried it:
class Brand < ActiveRecord::Base
extend Linkable
linkable 'identifier'
end
class Product < ActiveRecord::Base
extend Linkable
linkable 'name'
end
class Image < ActiveRecord::Base
belongs_to :linkable, :polymorphic => true
end
module Linkable
def linkable(identifier_name = 'name')
has_many :images, :as => :linkable
instance_eval do
define_method :link_identifier do
send identifier_name.to_sym
end
end
end
end
>> Product
=> Product(id: integer, name: string, created_at: datetime, updated_at: datetime)
>> Brand
=> Brand(id: integer, identifier: string, created_at: datetime, updated_at: datetime)
>> Brand.create :identifier => 'Foo'
=> #<Brand id: 1, identifier: "Foo", created_at: "2010-12-08 16:00:11", updated_at: "2010-12-08 16:00:11">
>> Product.create :name => 'Bar'
=> #<Product id: 1, name: "Bar", created_at: "2010-12-08 16:00:23", updated_at: "2010-12-08 16:00:23">
>> i = Image.new
=> #<Image id: nil, linkable_type: nil, linkable_id: nil, title: nil, created_at: nil, updated_at: nil>
>> i.linkable = Product.first
=> #<Product id: 1, name: "Bar", created_at: "2010-12-08 16:00:23", updated_at: "2010-12-08 16:00:23">
>> i.save
>> i = Image.new
=> #<Image id: nil, linkable_type: nil, linkable_id: nil, title: nil, created_at: nil, updated_at: nil>
>> i.linkable = Brand.first
=> #<Brand id: 1, identifier: "Foo", created_at: "2010-12-08 16:00:11", updated_at: "2010-12-08 16:00:11">
>> i.save
=> true
>> Image.first.link_identifier
=> "Bar"
>> Image.last.link_identifier
=> "Foo"
精彩评论