Книга: Metaprogramming Ruby 2
Назад: Chapter 11: The Rise and Fall of alias_method_chain
Дальше: The Fall of alias_method_chain

The Rise of alias_method_chain

In , I showed you a snippet of code from an old version of Rails…minus a few interesting lines. Here is the same code again, with those lines now visible and marked with arrows:

 
module​ ActiveRecord
 
module​ Validations
 
 
def​ self.included(base)
 
base.extend ClassMethods
*
base.class_eval ​do
*
alias_method_chain :save, :validation
*
alias_method_chain :save!, :validation
*
end
 
 
# ...
 
 
end

When ActiveRecord::Base includes the Validations module, the marked lines reopen Base and call a method named alias_method_chain. Let me show you a quick example to explain what alias_method_chain does.

The Reason for alias_method_chain

Suppose you have a module that defines a greet method. It might look like the following code.

 
module​ Greetings
 
def​ greet
 
"hello"
 
end
 
end
 
 
class​ MyClass
 
include Greetings
 
end
 
 
MyClass.new.greet ​# => "hello"

Now suppose you want to wrap optional functionality around greet—for example, you want your greetings to be a bit more enthusiastic. You can do that with a couple of Around Aliases ():

 
class​ MyClass
 
include Greetings
 
 
def​ greet_with_enthusiasm
 
"Hey, ​#{greet_without_enthusiasm}​!"
 
end
 
 
alias_method :greet_without_enthusiasm, :greet
 
alias_method :greet, :greet_with_enthusiasm
 
end
 
 
MyClass.new.greet ​# => "Hey, hello!"

I defined two new methods: greet_without_enthusiasm and greet_with_enthusiasm. The first method is just an alias of the original greet. The second method calls the first method and also wraps some happiness around it. I also aliased greet to the new enthusiastic method—so the callers of greet will get the enthusiastic behavior by default, unless they explicitly avoid it by calling greet_without_enthusiasm instead:

 
MyClass.new.greet_without_enthusiasm ​# => "hello"

To sum it all up, the original greet is now called greet_without_enthusiasm. If you want the enthusiastic behavior, you can call either greet_with_enthusiasm or greet, which are actually aliases of the same method.

This idea of wrapping a new feature around an existing method is common in Rails. In all cases, you end up with three methods that follow the naming conventions I just showed you: method, method_with_feature, and method_without_feature. The only the first two methods include the new feature.

Instead of duplicating these aliases all over the place, Rails provided a metaprogramming method that did it all for you. It was named Module#alias_method_chain, and it was part of the Active Support library. I’m saying “it was” rather than “it is” for reasons that will be clear soon—but if you look inside Active Support, you’ll find alias_method_chain is still there. Let’s look at it.

Inside alias_method_chain

Here is the code of alias_method_chain:

 
class​ Module
 
def​ alias_method_chain(target, feature)
 
# Strip out punctuation on predicates or bang methods since
 
# e.g. target?_without_feature is not a valid method name.
 
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ​''​), $1
 
yield​(aliased_target, punctuation) ​if​ block_given?
 
 
with_method = ​"​#{aliased_target}​_with_​#{feature}#{punctuation}​"
 
without_method = ​"​#{aliased_target}​_without_​#{feature}#{punctuation}​"
 
 
alias_method without_method, target
 
alias_method target, with_method
 
 
case
 
when​ public_method_defined?(without_method)
 
public target
 
when​ protected_method_defined?(without_method)
 
protected target
 
when​ private_method_defined?(without_method)
 
private target
 
end
 
end
 
end

alias_method_chain takes the name of a target method and the name of an additional feature. From those two, it calculates the name of two new methods: target_without_feature and target_with_feature. Then it stores away the original target as target_without_feature, and it aliases target_with_feature to target (assuming that a method called target_with_feature is defined somewhere in the same module). Finally, the case switch sets the visibility of target_without_feature so that it’s the same visibility as the original target.

alias_method_chain also has a few more features, such as yielding to a block so that the caller can override the default naming, and dealing with methods that end with an exclamation or a question mark—but essentially, it just builds an Around Alias (). Let’s see how this mechanism was used in ActiveRecord::Validations.

One Last Look at Validations

Here is the code from the old version of ActiveRecord::Validations again:

 
def​ self.included(base)
 
base.extend ClassMethods
 
# ...
 
base.class_eval ​do
 
alias_method_chain :save, :validation
 
alias_method_chain :save!, :validation
 
end
 
# ...
 
end

These lines reopen the ActiveRecord::Base class and hack its save and save! methods to add validation. This aliasing ensures that you will get automatic validation whenever you save an object to the database. If you want to save without validating, you can call the aliased versions of the original method, now called save_without_validation.

For the entire scheme to work, the Validations module still needs to define two methods named save_with_validation and save_with_validation!:

 
module​ ActiveRecord
 
module​ Validations
 
def​ save_with_validation(perform_validation = true)
 
if​ perform_validation && valid? || !perform_validation
 
save_without_validation
 
else
 
false
 
end
 
end
 
def​ save_with_validation!
 
if​ valid?
 
save_without_validation!
 
else
 
raise RecordInvalid.new(self)
 
end
 
end
 
def​ valid?
 
# ...

The actual validation happens in the valid? method. Validation#save_with_validation returns false if the validation fails, or if the caller explicitly disables validation. Otherwise, it just calls the original save_without_validation. Validation#save_with_validation! raises an exception if the validation fails, and otherwise falls back to the original save_with_validation!.

This is how alias_method_chain was used around the times of Rails 2. Things have changed since then, as I will explain next.

Назад: Chapter 11: The Rise and Fall of alias_method_chain
Дальше: The Fall of alias_method_chain