Если ваш класс разросся настолько, что начинает нарушать принцип единственной обязанности, вы без труда сможете разбить его на несколько более связных классов. Поможет вам в этом предоставляемая Ruby конструкция DelegateClass
.
Допустим, у вас есть класс Person
. Пользователи в системе могут продавать что-то и/или публиковать статьи. Подклассы здесь использовать не получится, потому что пользователь может одновременно быть и автором, и продавцом. Проведем рефакторинг.
Первоначально ваш класс выглядит как-то так:
class Person < ActiveRecord::Base
has_many :articles # статьи
has_many :comments, :through => :articles # комментарии
has_many :items # товары
has_many :transactions # продажи
# продавец?
def is_seller?
items.present?
end
# сколько ему должны
def amount_owed
# => хитрые вычисления
end
# автор?
def is_author?
articles.present?
end
# может постить на главную страницу?
def can_post_article_to_homepage?
# => сложная система разрешений на публикацию статей
end
end
Вроде бы, выглядит все неплохо. «Класс Person должен знать, сколько вещей продал пользователь и сколько статей опубликовал», скажете вы. «Чушь несусветная», отвечу я.
Поступает новая задача: пользователи могут быть как продавцами/авторами, так и покупателями. Чтобы выполнить эту задачу, вам нужно изменить свой класс, например вот так:
class Person < ActiveRecord::Base
# ...
has_many :purchased_items # купленные вещи
has_many :purchased_transactions # оплата покупок
# покупатель?
def is_buyer?
purchased_items.present?
end
# ...
end
Во-первых, вы нарушили принцип открытости/закрытости (открытости для расширения, но закрытости для изменений), ведь вы изменили класс. Во-вторых, при осуществлении продаж/покупок имена классов будут неочевидны («пользователь продает пользователю», было бы круче, если бы «продавец продавал покупателю»). И наконец, код нарушает принцип разделения ответственности.
Теперь представим, что поступила новая задача. Пользователи должны храниться не в реляционной, а, скажем, в NoSQL базе данных, или получаться с веб-сервиса через XML. Вы лишаетесь удобств ActiveRecord
, все эти has_many
больше не работают. По сути, вам нужно переписать класс с нуля, разработка нового функционала откладывается.
Знакомьтесь: DelegateClass
Вместо того, чтобы изменять класс Person
, его функциональность можно расширить с помощью классов-делегаторов:
# пользователь
class Person < ActiveRecord::Base
end
# продавец
class Seller < DelegateClass(Person)
delegate :id, :to => :__getobj__
# товары
def items
Item.for_seller_id(id)
end
# продажи
def transactions
Transaction.for_seller_id(id)
end
# продавец?
def is_seller?
items.present?
end
# сколько ему должны
def amount_owed
# => хитрые вычисления
end
end
# автор
class Author < DelegateClass(Person)
delegate :id, :to => :__getobj__
# статьи
def articles
Article.for_author_id(id)
end
# комментарии
def comments
Comment.for_author_id(id)
end
# автор
def is_author?
articles.present?
end
# может постить на главную страницу?
def can_post_article_to_homepage?
# => сложная система разрешений на публикацию статей
end
end
Для использования этих классов придется написать чуть больше кода. Вместо
person = Person.find(1)
person.items
используйте такой код:
person = Person.find(1)
seller = Seller.new(person)
seller.items
seller.first_name # => вызывается person.first_name
Теперь совсем несложно сделать пользователей еще и покупателями:
# покупатель
class Buyer < DelegateClass(Person)
delegate :id, :to => :__getobj__
# купленные товары
def purchased_items
Item.for_buyer_id(id)
end
# покупатель?
def is_buyer?
purchased_items.present?
end
end
Теперь если вам придется переехать с ActiveRecord на Mongoid, вам не нужно ничего менять в классах-делегаторах.
Разумеется, классы-делегаторы — не серебряная пуля. Нужно какое-то время, чтобы привыкнуть к не всегда очевидному поведению некоторых методов, например, #reload
:
person = Person.find(1)
seller = Seller.new(person)
seller.class # => Seller
seller.reload.class # => Person
Еще один подводный камень состоит в том, что по умолчанию метод #id
не делегируется. Чтобы получать именно AcitveRecord#id
, добавьте эту строчку в класс-делегатор:
delegate :id, :to => :__getobj__
Несмотря на это, классы-делегаторы — отличный инструмент для повышения гибкости кода.
От переводчика: Сергей Потапов указывает на еще одну неочевидную особенность
DelegateClass
:
require 'delegate'
class Animal
end
class Dog < DelegateClass(Animal)
end
animal = Animal.new
dog = Dog.new(animal)
dog.eql?(dog) # => false, WTF? O_o
Происходит это из-за того, что #eql?
вызывается для базового объекта (в данном случае, animal
):
dog.eql?(animal) # => true
С другой стороны, #equal?
не делегируется по умолчанию:
dog.equal?(dog) # => true
dog.equal?(animal) # => false
Автор: kaluzhanin