开发者

What is the best practice to get the same behavior from multiple AR objects that are referred to polymorphically?

开发者 https://www.devze.com 2023-01-30 02:12 出处:网络
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.

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"
0

精彩评论

暂无评论...
验证码 换一张
取 消