📜 ⬆️ ⬇️

Improved plugin idioms

This post was published on November 12, 2009, but I think it has not lost its relevance, because the plugins for Rails (and not only) are still relevant.

Karl and I have been working on a plugin system for the last days. In particular, we walked the Rails Plugin Guide. Reading the guide, we noticed a lot of excesses in the idioms represented there.

I do not reproach the author of the guide; the idioms presented are exactly the same as those used since the earliest days of Rails. On the other hand, looking at them, I recalled those days when, at the sight of such a code, it seemed to me that Ruby is full of magic spells and relatively simple things require some special ceremonies (like a tambourine dance. - Appro. Transl. ).
')
Here is an example:
Copy Source | Copy HTML<br/> module Yaffle<br/> def self .included(base)<br/> base.send :extend, ClassMethods<br/> end <br/> <br/> module ClassMethods<br/> # , , Hickwall <br/> def acts_as_something <br/> send : include , InstanceMethods<br/> end <br/> end <br/> <br/> module InstanceMethods<br/> # , , @hickwall <br/> end <br/> end <br/>

Let's start with the fact that send is not needed at all. The acts_as_something method will be called in the class itself, which will give it access to the private include method.


This code will be used as follows:
Copy Source | Copy HTML<br/> class ActiveRecord::Base <br/> include Yaffle<br/> end <br/> <br/> class Article < ActiveRecord::Base <br/> acts_as_yaffle<br/> end <br/>

This code
  1. Registers a hook so that when the module is included, the class is extended by methods from ClassMethods
  2. In it (in ClassMethods ) declares a method that includes InstanceMethods
  3. So that you can use acts_as_something in your code.
The abnormal thing in all of this is that it reinvents the system of modules that Ruby already has. It would be completely identical:

Copy Source | Copy HTML<br/> module Yaffle<br/> # , , Hickwall <br/> def acts_as_something <br/> send : include , InstanceMethods<br/> end <br/> <br/> module InstanceMethods<br/> # , , @hickwall <br/> end <br/> end <br/>

Then to use in:
Copy Source | Copy HTML<br/> class ActiveRecord::Base <br/> extend Yaffle<br/> end <br/> <br/> class Article < ActiveRecord::Base <br/> acts_as_yaffle<br/> end <br/>

In a nutshell, there is no point in overriding the include so that it behaves like extend if Ruby has both of them!

You can do:
Copy Source | Copy HTML<br/> module Yaffle<br/> # , , @hickwall, <br/> # , ! <br/> end <br/> <br/>

Then to use in:
Copy Source | Copy HTML<br/> class Article < ActiveRecord::Base <br/> include Yaffle<br/> end <br/> <br/>

In fact, the initial code (override of hook for inclusion to extend the class via extend , which further includes the module) is two layers of abstraction around a simple inclusion in Ruby!

Let's look at some more examples:
Copy Source | Copy HTML<br/> module Yaffle<br/> def self .included(base)<br/> base.send :extend, ClassMethods<br/> end <br/> <br/> module ClassMethods<br/> def acts_as_yaffle (options = {})<br/> cattr_accessor :yaffle_text_field<br/> self .yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s<br/> end <br/> end <br/> end <br/> <br/> ActiveRecord::Base .send : include , Yaffle <br/>

Again, the override idiom include , so that it behaves like extend (instead of just calling extend !).

Better solution:
Copy Source | Copy HTML<br/> module Yaffle<br/> def acts_as_yaffle (options = {})<br/> cattr_accessor :yaffle_text_field<br/> self .yaffle_text_field = options[:yaffle_text_field].to_s || "last_squawk" <br/> end <br/> end <br/> <br/> ActiveRecord::Base .extend Yaffle <br/>

In this case, you should use acts_as_yaffle , since we offer additional options that could not be encapsulated using normal extend. (Mysterious phrase. In the original: In this case, it’s appropriate to use it. Please note ) .

Another "more advanced" case:

Copy Source | Copy HTML<br/> module Yaffle<br/> def self .included(base)<br/> base.send :extend, ClassMethods<br/> end <br/> <br/> module ClassMethods<br/> def acts_as_yaffle (options = {})<br/> cattr_accessor :yaffle_text_field<br/> self .yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s<br/> send : include , InstanceMethods<br/> end <br/> end <br/> <br/> module InstanceMethods<br/> def squawk (string)<br/> write_attribute( self . class .yaffle_text_field, string.to_squawk)<br/> end <br/> end <br/> end <br/> <br/> ActiveRecord::Base .send : include , Yaffle <br/>

Again, include an override to extend , and call send , although this is not necessary. Identical functionality:
Copy Source | Copy HTML<br/> module Yaffle<br/> def acts_as_yaffle (options = {})<br/> cattr_accessor :yaffle_text_field<br/> self .yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s<br/> include InstanceMethods<br/> end <br/> <br/> module InstanceMethods<br/> def squawk (string)<br/> write_attribute( self . class .yaffle_text_field, string.to_squawk)<br/> end <br/> end <br/> end <br/> <br/> ActiveRecord::Base .extend Yaffle <br/>

Of course, you can do this:
Copy Source | Copy HTML<br/> module Yaffle<br/> def squawk (string)<br/> write_attribute( self . class .yaffle_text_field, string.to_squawk)<br/> end <br/> end <br/> <br/> class ActiveRecord::Base <br/> def self .acts_as_yaffle(options = {})<br/> cattr_accessor :yaffle_text_field<br/> self .yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s<br/> include Yaffle<br/> end <br/> end <br/>

Since modules are always included in ActiveRecord::Base , the previous code with additional modules and use extend not worse than simply reopening the class and adding the acts_as_yaffle method directly. Now you can put the squawk method right inside the Yaffle module, from where it can easily be embedded.

Maybe this is not very important, but the amount of deceptive magic in the template for writing plug-ins is significantly reduced, making it more accessible to the user. In addition, the new user has the ability to quickly penetrate the work of include and extend without the false impression of the need for magic spells, the use of send and special modules such as ClassMethods to make the plugins work.

To be clear, I am not saying that these idioms are not needed in some special, advanced cases. On the other hand, I say that in the most common cases, they heavily clutter up the code, which hides the real functionality and leads the user to a dead end.

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


All Articles