- Following conventions can reveal opportunities for metaprogramming.
- Conventions make it easier to find where behavior is defined. This reason strongly applies as long as we have lacking IDE support.
belongs_toare perfect examples of something that needs to be done in several locations within a codebase, but can be succinctly done thanks to metaprogramming.
Metaprogramming isn't exclusive to Rails. I recently joined a project where the class under test needed to be required from within the test. I've been spoiled by Rails, so having to write require statements was annoying to me. To solve the problem, I convinced the team to build their test directory structure as a mirror to the lib directory structure. I also convinced them that using dust to define tests was a good idea. Once we got all the tests using the
unit_test do .. endsyntax it was easy for me to use the binding of that block to get the path to the test. Since our test directory structure mirrored the lib directory, I was able to
gsubthe path to the test and require the class under test behind the scenes.
Removing require statements is nice, but it's also obviously a small win. However, it doesn't take long for several small metaprogramming wins to greatly increase the effectiveness of a team. A few projects ago, our small wins ended up becoming a domain specific framework built on top of Rails. A consequence of combining all the small wins was that adding to the application was very trivial. That team saw tremendous effectiveness gains with every story that required adding new a new screen.
Last week while pairing with Fred George I asked what he thought of adding a not method to improve readability. He liked the readability, but was concerned with being able to find the implementation of the
notmethod. I think that's a valid concern, but one that can be mitigated by following good conventions. On our project we have a folder called "core_extensions" which groups files such as string_extension.rb, object_extension.rb, etc. I've found this convention helpful because I can generally find behavior after looking in a few classes. For example, I may stumble upon the following code.
payment_connection.establish(:paypal) if payment_connection.not.active?
To find the implementation of
notI'll look in the connection class first since the connection is the receiver of the
@active = true
Looking in Connection will not provide the implementation of
not, but it does provide other valuable information. Notably, Connection is a PORO; therefore the ancestors of Connection are Object and Kernel.
Connection.ancestors # => [Connection, Object, Kernel]
At this point, life is easy. I only need to look in object_extensions.rb and then kernel_extensions.rb if necessary.
But, life was only easy because we follow conventions. I can easily see creating a readability.rb file seeming like a good idea. After all, object_extensions.rb doesn't do much in the way of describing intent. I am a big fan of software that focuses on intent, but I actually think it's destructive in this case if it means breaking convention. Of course, if my IDE could easily find method definitions I would favor an intentful filename.
People new to Ruby are generally afraid of Open Classes. Convention can go a long way to mitigating the risks of Open Classes. In January, I listed a few Class Reopening Hints. These days we follow roughly the same conventions and I still find myself spending very little time looking for implementation definitions; thanks mostly to convention.