📜 ⬆️ ⬇️

I fell in love with DelegateClass

If your class has grown so much that it begins to violate the principle of a single duty , you can easily break it into several more cohesive classes. The Ruby construction of DelegateClass will help you in this.

Suppose you have a class Person . Users in the system can sell something and / or publish articles. Subclasses here will not work, because the user can be both an author and a seller at the same time. We'll refactor.

Initially, your class looks something like this:
 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 

It seems that everything looks good. “The Person class must know how many things the user has sold and how many articles he has published,” you say. "Utter nonsense," I answer.

A new challenge arrives: users can be both sellers / authors and buyers. To accomplish this task, you need to change your class, like this:
 class Person < ActiveRecord::Base # ... has_many :purchased_items #   has_many :purchased_transactions #   # ? def is_buyer? purchased_items.present? end # ... end 

First, you violated the principle of openness / closeness (openness for expansion, but closure for change), because you changed the class. Secondly, when making sales / purchases, the names of the classes will not be obvious (“the user sells to the user”, it would be better if “the seller sold to the buyer”). Finally, the code violates the principle of sharing responsibility .
')
Now imagine that a new task has arrived. Users should not be stored in relational, but, say, in a NoSQL database, or obtained from a web service via XML. You lose the convenience of ActiveRecord , all these has_many no longer work. In fact, you need to rewrite the class from scratch, the development of a new functionality is postponed.

Meet DelegateClass

Instead of modifying the Person class, its functionality can be extended with delegate classes:
 #  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 

To use these classes will have to write a little more code. Instead
 person = Person.find(1) person.items 

use the following code:
 person = Person.find(1) seller = Seller.new(person) seller.items seller.first_name # =>  person.first_name 

Now it is not difficult to make users also buyers:
 #  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 

Now if you have to move from ActiveRecord to Mongoid, you don’t need to change anything in the delegate classes.

Of course, delegate classes are not a silver bullet. It takes some time to get used to the not always obvious behavior of some methods, for example, #reload :
 person = Person.find(1) seller = Seller.new(person) seller.class # => Seller seller.reload.class # => Person 

Another caveat is that the default #id method is not delegated. To get AcitveRecord#id , add this line to the delegate class:
 delegate :id, :to => :__getobj__ 

Despite this, delegate classes are a great tool for increasing code flexibility.


From the translator : Sergey Potapov points to another non-obvious feature of 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 

Is this due to #eql? called for the base object (in this case, animal ):
 dog.eql?(animal) # => true 

On the other hand, #equal? not delegated by default:
 dog.equal?(dog) # => true dog.equal?(animal) # => false 

Source: https://habr.com/ru/post/149454/


All Articles