Polymorphic Association

July 01, 2012

有这么一个需求,一个在线音乐商店系统,我们暂且叫它’online-store’,需要记录消费者对每首歌的评论,一个有经验的rails developer会很快地写出下面的代码:

class Music
  has_many :comments
end

class Comment
  belongs_to :music
end

对应的表结构如下:

#musics:   
  |  id  | integer|   
  | name | varchar|   
  | ...  |  ...   |   
#comments:   
  | id      | integer|   
  | text    | varchar|   
  | music_id| integer|   

如果需要给music添加,查看评论,可以通过如下代码实现:

  #添加评论
  music.comments.create {:text => 'this is realy nice'}
  #查看评论
  music.coments

风云变幻,斗转星移,apple的app store创造了软件销售的新模式,我们的vendor也坐不住了,决定在现有的音乐商店系统上出售应用程序,电影,游戏等内容,同样,这些内容也需要支持评论,有了前面成功的经验,你信心满满增加了下面几个model:

  class Application
    has_many :comments
  end

  class Movie
    has_many :comments
  end

  class Game
    has_many :comments
  end

再来看看我们之前写的model Comment:

class Comment
  belongs_to :music
end

现在需要支持多种内容,而且这些类内容之间出了都可以被评论外,再无其他关联,那这个belongs_to该怎么写呢?一个最直接的思路是,扩展Comment,让其支持对以上四类内容的评论,代码如下:

class Comment
  belongs_to :music, though => "music_id"
  belongs_to :game, though => "game_id"
  belongs_to :application, though => "application_id"
  belongs_to :movie, though => "movie_id"
end

表结构如下:

#comments:   
  | id             | integer|   
  | text           | varchar|   
  | music_id       | integer|   
  | game_id        | integer|
  | application_id | integer|
  | movie_id       | integer|

有了以上的model,你就可以给应用程序,电影,游戏增加评论了:

  #创建评论
  application.comments.create {:text => "this is a nice app"}
  movie.comments.create {:text => "this is a nice movie"}
  game.comments.create {:text => "this is a nice game"}
  #查看评论
  application.comments
  movie.comments
  game.comments

目前看来,这些代码工作得很好,然而,做为一个有着良好直觉的程序员,你敏锐地觉察到,将来可能有更多的内容出现在这个”onlne-store”中,也会有更多的内容需要支持评论——你成功地识别出一个”易变点“,每当新增一种内容的时候,你就需要打开这个Comment类,新增一个association,同时,还需要增加migration,这个设计明显违背了开闭原则”。那么,这个问题该怎么解决呢? 再来分析下这个问题,上面我们提到,这些model唯一的共性是 “可以被评论”,于是我们可以抽象出一个概念——“commentable”。如果我们让comment对象知道它所对应的”commentable”对象的id以及类型(game/application/movie),我们就可以获得一个“commentable”对象的所有comments,参考下面的代码:

  #查看id为1的music的评论
  Comment.find(:commentable_id => 1, :commentable_type => 'music')

  #查看id为1的application的评论
  Comment.find(:commentable_id => 1, :commentable_type => 'application')

如此,comments的表结构就可以简化为:

#comments:   
  | id              | integer|   
  | text            | varchar|   
  | commentable_id  | integer|   
  | commentable_type| varchar|

model代码简化为:

 #添加评论
 Comment.create({:text => "good staff", :commentable_id => "1", :commentable_type => "music"})
 #查看评论
 Comment.find(:commentable_id => 1, :commentable_type => 'music')
  class Comment
    belongs_to :commentable, :polymorphic => true
  end
  class Application
    has_many :comments, :as => :commentable
  end
  class Movie
    has_many :comments, :as => :commentable
  end
  class Game
    has_many :comments, :as => :commentable
  end
  #添加评论
  movie.comments.create {:text => 'this is realy nice'} 
  #查看评论
  movie.coments

更多关于“polymorphic association”的信息,请参考这里 怎么样,这样一来,再新增多少种内容类型,处理起来都非常容易,扩展下commentable_type的值就可以了。这个思路的根本出发点在于为识别、分离变化点。这类问题可以抽象为这样一个问题:”如何把一个概念应用到一组除了这个概念,没有其他任何关联的对象上?” ,此类问题可以采用上述思路解决。

readable angular tests

In this post, I'll provdes some tips to create readable angular test.### General Tips#### Use describe or nested describe to structure sp...… Continue reading

Exposing event stream from monolith

Published on September 18, 2017