DRY - это одна из концепций (или, как модно говорить, паттернов), расшифровывается как Don't Repeat Yourself. Если инициализировать три значения, так в цикле. Если есть общая функциональность в двух классах - вынести отдельно. И это ruby way.
Пусть у нас на сайте есть фотки и видео:
class Photo < ActiveRecord::Base
belongs_to :author, :foreign_key => :author_id
validates_presence_of :author_id
belongs_to :album, :foreign_key => :album_id
has_one :comment_list, :foreign_key => :id
has_many :comments, :through => :comment_list, :class_name => 'Comment'
end
class Video < ActiveRecord::Base
belongs_to :author, :foreign_key => :author_id
validates_presence_of :author_id
belongs_to :playlist, :foreign_key => :playlist_id
has_one :comment_list, :foreign_key => :id
has_many :comments, :through => :comment_list, :class_name => 'Comment'
end
То есть у каждой фотки есть автор, она в альбоме, и она имеет некоторый набор комментариев. У каждой видюшки есть автор, она в плейлисте, и она имеет некоторый набор комментариев.
Дальше. При создании фотки мы хотим автоматом создать и комментарии к ней (c помощью хука after_initialize, например):
class Photo
after_initialize :create_comments
private
def create_comments
self.comment_list ||= CommentList.new.save
end
end
И то же самое мы делаем для видео.
Стоп.
Количество одинакового кода должно перейти в качество. Вынесем-ка "комментируемость" и "принадлежность автору" отдельно. Но как?
1. Наследование.
Самый простой способ -
class SocialEntity < ActiveRecord::Base
belongs_to :author, :foreign_key => :author_id
validates_presence_of :author_id
has_one :comment_list, :foreign_key => :id
has_many :comments, :through => :comment_list, :class_name => 'Comment'
after_initialize :create_comments
private
def create_comments
self.comment_list ||= CommentList.new.save
end
end
class Photo < SocialEntity
belongs_to :album, :foreign_key => :album_id
end
class Video < SocialEntity
belongs_to :playlist, :foreign_key => :playlist_id
end
Однако, это не сработает. ActiveRecord попытается найти таблицу social_entities и обломится.
Впрочем, мы можем ее создать. И даже хранить там и фотки, и видео. Этот подход называется Single Table Inheritance и описан тут.
2. Mixin.
Это интересный способ:
class Photo < ActiveRecord::Base
include SocialEntity
belongs_to :album, :foreign_key => :album_id
end
class Video < ActiveRecord::Base
include SocialEntity
belongs_to :playlist, :foreign_key => :playlist_id
end
module SocialEntity
belongs_to :author, :foreign_key => :author_id, :class_name => 'User'
has_one :comment_list, :foreign_key => :id
has_many :comments, :through => :comment_list, :class_name => 'Comment'
validates_presence_of :author_id
after_initialize :create_comments
private
def create_comments
self.comment_list ||= CommentList.new.save
end
end
То есть общую функциональность мы оформляем в виде модуля и примешиваем к нашим классам. Классы при этом наследуются от ActiveRecord::Base и соотносятся каждый со своей таблицей, как и вначале. Хорошо, но опять не работает! Модуль не знает, что такое belongs_to, after_initialize и другие вкусные вещи ActiveRecord. И require 'active_record' не помогает.
Решение подсказали тут . Вместо описания нужных методов прямо в модуле, опишем их при его присоединении к классу!
module SocialEntity
def self.included(base)
base.belongs_to :author, :foreign_key => :author_id, :class_name => 'User'
base.has_one :comment_list, :foreign_key => :id
base.has_many :comments, :through => :comment_list, :class_name => 'Comment'
base.validates_presence_of :author_id
base.after_initialize :create_comments
end
private
def create_comments
self.comment_list ||= CommentList.new.save
end
public
end
Вот так.
Теперь, если к новой сущности Post тоже понадобятся эти функции (автор и комментарии), написать это будет просто:
class Post < ActiveRecord::Base
include SocialEntity
end
P.S. Есть и третий способ. Можно модифицировать ActiveRecord::Base и включить к него что-нибудь вроде хелпера has_social_features, наряду с has_one и belongs_to. Так мы на руби пришли к перловому принципуTIMTOWTDI.