IMO
Delegate in Ruby and Rails
When a class is too bloated because all the requirements are packed, you probably want to split it up. When designing tables, you may want to normalize and split it into separate tables. There are the other ways in Rails to split like concerns, services, and so on.
Delegation is one of these separation patterns, and Ruby's default gem includes delegate and forwardable. However, since Ruby is a language that can be dynamically rewritten in various ways, I have the impression that the formal delegation seen in statically-typed languages is not used very often. I think it is more common for Ruby to rewrite itself casually.
Active Support's delegate is more commonly used in Rails development than the default gem delegate. Demeter's law is sometimes cited as a reason why you should use this delegate, but I have encountered code that was so abused that it was difficult to decipher. I personally think that it is better not to use it at many times.
I would like to discuss what Demeter's Law was trying to say, whether Active Support's delegate fits the pattern, and whether it is right to use delegate without thinking about it.
What is Active Support's delegate?
Here is a simple example of using Active Support's delegate. You can write @category.title
instead of @category.blog_article.title
.
class BlogArticle < ApplicationRecord def title 'awesome blog title' end end class BlogArticleCategory < ApplicationRecord belongs_to :blog_article delegate :title, to: :blog_article end @category = BlogArticleCategory.find(1) @category.title
Many official references and Web articles give examples of Active Record use, but it can be used for other purposes as well. In Discourse's code base, it's also used for libs and serializers.
## From the official doc class Foo CONSTANT_ARRAY = [0,1,2,3] @@class_array = [4,5,6,7] def initialize @instance_array = [8,9,10,11] end delegate :sum, to: :CONSTANT_ARRAY delegate :min, to: :@@class_array delegate :max, to: :@instance_array end Foo.new.sum # => 6 Foo.new.min # => 4 Foo.new.max # => 11
The Demeter's Law is often introduced as a rationale for using this delegate. Is it true?
What is Demeter's Law?
Demeter's Law is a theory proposed by Ian Holland in 1987. It is not Demeter's Law because it was proposed by Demeter. It originated from the Demeter Project, which was researching aspect-oriented programming.
It is an idea similar to Agile, which was probably innovative at the time, and seems to have taken the name of a fertility goddess, as software is not something to build but something to grow.
Demeter's Law can be simply expressed as “Only talk to your friends". In object-oriented languages, it is common to use dots to trace related concepts, but the rule is "use only one dot".
Simply put, a.b().c()
is against the rule, but a.b()
is OK, meaning that instead of accessing c by traversing from a to b, the internal implementation should be hidden (by creating a wrapper method, for example) from the side that calls A.
At first glance, Active Support's delegate seems to satisfy the rule by creating a shortcut.
What was the point of Demeter's law?
If you believe Wikipeida, 1987 seems to be the year of its proposal, but the earliest paper available was Object-oriented programming: an objective sense of style in 1988, so I will proceed below based on the contents of that paper.
The structure of the paper is as follows
- Introduction
- Notation
- The Law of Demeter
- The Motivation and Explanation
- Example
- The Trade-off
- The Interface
- The Weak and Strong Law of Demeter
- Conforming to the Law
- Compile Time Checking of the Law
- Minimum Documentation
- Formulations of the Law
- Conclusion
The first thing that stands out is the Notation section. When creating a programming language itself, it is common to define its syntax in BNF notation, etc. Demeter notation was created based on EBNF notation. Demeter notation expresses a hierarchy of classes, not syntax. This is how it would have looked like if the concept similar to TypeScript's types were expressed in the way it was done at that time.
Reference-Books-Sec = <ref-books> List-of-Books <ref-catalog> Catalog. List-of-Books ~ {Book}. Catalog ~ {Catalog-Entry}. Book = <title> String <author> String <id> Book-identifiier
The point is that it is based on an object-oriented language with inheritance mainly talking about parent-child relationships.
Demeter's law can be written in a formal way like this.
For every method M of every class C, every object of every message sent by M must be an instance of the following class.
- Argument classes of M (including C)
- instance variable of C
Difficult.... To summarize, it seems to be CONVERSATION ONLY WITH YOUR CLOSEST FRIENDS as mentioned earlier.
Advantages and Disadvantages of Demeter's Law
The following are the benefits of using this law
- Control of connectivity
- Information hiding
- Restriction of information
- Localization of information
- Minimization of interfaces
- Easier to infer structure
Conversely, the disadvantage is that it hides the following parts of the grandchildren, so the number of methods in the child classes increases.
The paper also talks about strong Demeter's law and weak Demeter's law, and it seems that when instance variables are used in a class that inherits from another class, the decision is divided whether original class is under the rules of Demeter's law or not.
Does Active Support's delegate fit the pattern of Demeter's Law?
Based on the above, does Active Support's delegate fit the pattern of Demeter's Law? My own view is that Active Support's delegate does not always fit the pattern of Demeter's Law.
Certainly, using Active Support's delegate, it is possible to comply with the "use only one dot” rule, which makes a.b().c()
into a.b()
. On the other hand, the original paper of Demeter's Law seemed to be aware of class hierarchy and parent-child relationships. I thought that the a in a.b().c()
was assumed to be more often the parent.
On the other hand, Active Support's delegate is used to refer to a parent method from a child of belongs_to by reference. The direction of dependency is opposite.
Also, the most common use case of delegate between Active Record is between models in a Clean Architecture style layered structure, which means that they are used as neighbors rather than as parent-child relationships.
In my personal experience, in cases where the column name of a table is passed directly to the delegate of a related model, if a two-way delegate dependency is created from various tables, the code is not clear and refactoring is difficult.
When should I use delegate?
I think it is best to avoid the use of delegates in Active Record, and to limit the use of delegates to references from the main table of a domain to its children.
As the size of the program grows, the code in the Active Record or model layer may become too fat and difficult to read. In such cases, moving the Form Object, ViewModel, or other processing written in the model to a higher-level wrapper concept can improve the visibility of the code.
The concept of accepts_nested_attributes_for used in Rails Form is sometimes said to be too magical and should not be used, but I believe that delegate is another area where the magic of Rails is strong, and it can be a stumbling block when refactoring.
- Avoid using delegates in Active Record.
As the relationships and functions of models become more complex, they become more modular, and it is tempting to invent higher-level concepts of models.
It is more prospective to give birth to concepts that integrate and process multiple models. Short-hand notation is incompatible with refactoring and should be avoided.
- When using delegate in Active Record, use only the main table of the domain.
What we wanted to accomplish in the original paper of Demeter's Law was to wrap the child part of the parent-child relationship, so we did not delegate from child to parent, and in the case of BlogArticle, the BlogArticle method wraps the complexity of the BlogArticleCategory.
By doing so, the direction of dependency can be narrowed down to a single direction, and if the code of BlogArticle becomes too bloated in the future, it will be easy to transfer it to a higher level concept.
Also, for the user-related tables, we do not use delegate because we want the references from the function tables to the user-related tables to be loosely coupled. This is because the code visibility will be improved if the function and user axes are loosely coupled, and it is also possible to cope with the case where the user DB is divided in the future.
As another solution, if you are using a decorator-type gem, it may be easier to refactor in the future by putting the short-hand part in the decorator.
Finally
These are my personal views. I write a lot of Python, so I may be a Rubyist who likes to write explicitly.
It is hard for me because I have not had many members understand that it is useful to prohibit the use of delegate. In such cases, it may be a good idea to limit the use of delegates to parent-to-child references.