Если ваш класс разросся настолько, что начинает нарушать принцип единственной обязанности, вы без труда сможете разбить его на несколько более связных классов. Поможет вам в этом предоставляемая Ruby конструкция
Допустим, у вас есть класс
Первоначально ваш класс выглядит как-то так:
Вроде бы, выглядит все неплохо. «Класс Person должен знать, сколько вещей продал пользователь и сколько статей опубликовал», скажете вы. «Чушь несусветная», отвечу я.
Поступает новая задача: пользователи могут быть как продавцами/авторами, так и покупателями. Чтобы выполнить эту задачу, вам нужно изменить свой класс, например вот так:
Во-первых, вы нарушили принцип открытости/закрытости (открытости для расширения, но закрытости для изменений), ведь вы изменили класс. Во-вторых, при осуществлении продаж/покупок имена классов будут неочевидны («пользователь продает пользователю», было бы круче, если бы «продавец продавал покупателю»). И наконец, код нарушает принцип разделения ответственности.
Теперь представим, что поступила новая задача. Пользователи должны храниться не в реляционной, а, скажем, в NoSQL базе данных, или получаться с веб-сервиса через XML. Вы лишаетесь удобств
Вместо того, чтобы изменять класс
Для использования этих классов придется написать чуть больше кода. Вместо
используйте такой код:
Теперь совсем несложно сделать пользователей еще и покупателями:
Теперь если вам придется переехать с ActiveRecord на Mongoid, вам не нужно ничего менять в классах-делегаторах.
Разумеется, классы-делегаторы — не серебряная пуля. Нужно какое-то время, чтобы привыкнуть к не всегда очевидному поведению некоторых методов, например,
Еще один подводный камень состоит в том, что по умолчанию метод
Несмотря на это, классы-делегаторы — отличный инструмент для повышения гибкости кода.
От переводчика: Сергей Потапов указывает на еще одну неочевидную особенность
Происходит это из-за того, что
С другой стороны,
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
