Today, I pair with Ma Wei to refactor a piece of pre-existed code. We try to eliminate some “static methods” (in fact, there is no real static method in ruby, I use this term to describe the methods that only depends on its parameters other than any instance variables).
The code is like this:
|
|
As you can see the class Recruiter is used as a host for the methods that manipulate the array of candidates, which is a strong bad smell . So we decide to move these methods to their context class.
In Java or C#, the solution to this smell is quite obvious, which could be announced as “Standard Answers”:
- Mark all methods static.
- Create a new class named CandiateCollection.
- Change the type of candidates to CandidateCollection.
- Mark all methods non-static, and move it to CandidateCollection class.
If you use Resharper or IntelliJ enterprise version, then the tool can even do this for you.
But in ruby world, or even in dynamic language world, we don’t like to create so many classes, especially these “strong-typed collection”. I wish I could inject these domain related methods to the array instance when necessary, which is known as “singleton methods” in ruby.
To achieve this, I might need the code like this:
|
|
With the help of this wrap_array
method, we can dynamic inject the method into the array like this:
|
|
This is cool, but still not cool enough. We still have problems:
- All the business logic is included in the wrap method. It is hard to maintain.
- Where should we declare this wrap method? In class Array or another “static class”?
The answer to the 1st question is easy, our solution is encapsulate these logics into a module:
|
|
By encapsulate the logic into a module, then we can extract it into a single file, so the logic could be organized in the way as we want.
Now we need to solve the second problem and reuse the module we just created.
To achieve this, we wrote the following code:
|
|
In the code, we re-opened the class Array, and define a new method called to_candidate_collection
, which is used to inject domain methods into a generic array.
So we can have the following code:
|
|
Now our refactoring is basically completed.
But soon, we realize that is pattern is really powerful and should be able to be reused easily. So we decide to move on.
We want to_candiate_collection
be more generic, so we can dynamically inject any module, not just CandidateCollection.
So we wrote the following code:
|
|
So we can have the code like this:
|
|
The code looks cool, but failed to run.
The reason is that we opened the meta class of the instance, which means we enter another level of context, so the parameter module is no longer visible.
To solve this problem, we need to flatten the context by using closure. So we modified the code as following:
|
|
The code metaclass = class << self; self; end
is very tricky, we use this statement to get the meta class of the array instance.
Then we call class_eval on meta class, which then mixed-in the module we want.
Now the code is looked nice. We can dynamically inject any module into “Array” instance.
Wait a minute, why only “Array”? We’d like to have this capability on any object!
Ok, that’s easy, let’s move the method to Kernel module, which is mixed-in by Object class.
|
|
Now we can say the code looks beautiful.
NOTICE:Have you noticed that we have a self
expression at the end of the dynamic_inject
method as return value.
This statement is quite important!
Since we will get “undefined method error” when calling Candidate.scoped_by_id(candidate_ids).dynamic_inject(CandidateCollection).approve!
if we missed this statement.
We spent almost 1 hour to figure out this stupid mistake. It is really a stupid but expensive mistake!
Instead of these tricky ways, for Ruby 1.9+, it is okay to use extend
method to replace the tricky code.
The extend
method is the official way to do “dyanamic inject” as described before.