DRY Your Rails Code with Singleton Class Methods and Metaprogramming

At Clutter, our business is experiencing fantastic growth. As our customer base increases, so does the demand on our Ruby on Rails application. It’s more important than ever that we keep our codebase well factored and open to change.
A Guiding Principle
One of the first refactoring concepts that we learn as new Rails engineers is DRY — Don’t Repeat Yourself. The guides identify DRY as Rails’ first guiding principle:
DRY is a principle of software development which states that “Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.” By not writing the same information over and over again, our code is more maintainable, more extensible, and less buggy.
Code that adheres to the DRY principle is easy, and therefore less costly, to change. If you write a particular piece of code in only one place, inevitable change won’t require shotgun surgery.
A Common Example
Examples of DRY usually involve performing extract method on code common to two methods. Consider a controller with edit and update actions.
class BooksController < ApplicationController
def edit
@book = Book.find(params[:id])
end def update
@book = Book.find(params[:id]) if @book.update_attributes(book_params)
flash[:success] = "Book updated"
redirect_to @book
else
render 'edit'
end
end private def book_params
params.require(:book).permit(:title, :author, :isbn)
end
end
It’s easy to spot the code common to edit and update:
@book = Book.find(params[:id])DRYing up the code is as simple as extracting that code to a method and calling the new method.
class BooksController < ApplicationController
def edit
find_book
end def update
find_book if @book.update_attributes(book_params)
flash[:success] = "Book updated"
redirect_to @book
else
render 'edit'
end
end private def find_book
@book = Book.find(params[:id])
end def book_params
params.require(:book).permit(:title, :author, :isbn)
end
end
We’ve encapsulated the book finding logic in a method. Now, if we need to change the way we find a book, we only have to change code in one place.
We can even take it a step further and DRY up the calls to our new find_book method.
class BooksController < ApplicationController
before_action :find_book def edit
end def update
if @book.update_attributes(book_params)
flash[:success] = "Book updated"
redirect_to @book
else
render 'edit'
end
end private def find_book
@book = Book.find(params[:id])
end def book_params
params.require(:book).permit(:title, :author, :isbn)
end
end
Now we’ve encapsulated the name of the method that in turn encapsultates the book finding logic. If we change the method name, we only have to change it in one place. Moreover, if we want to add a new action to the controller, like show, it will automatically pick up on our find_book method.
class BooksController < ApplicationController
before_action :find_book def show
end def edit
end def update
if @book.update_attributes(book_params)
flash[:success] = "Book updated"
redirect_to @book
else
render 'edit'
end
end private def find_book
@book = Book.find(params[:id])
end def book_params
params.require(:book).permit(:title, :author, :isbn)
end
end
A Rails Magic Trick
It’s easy to miss, but Rails just performed a magic trick for us. We used a method that we ourselves defined to encapsulate the book finding logic above. Similarly, we used a method that Rails defines to encapsulate the name of the book finding method.
Consider the following except from the controller code:
before_action :find_bookIn Ruby, parentheses around arguments in method calls are optional. So the above code could also be written as:
before_action(:find_book)before_action is actually a call to a method, passing :find_book as an argument. Rails defines before_action on AbstractController::Callbacks::ClassMethods, which is accessable from our BooksController class because it inherits from ApplicationController.
Put in line, it almost looks like the following:
class BooksController
class << self
def before_action(method_name)
# method implementation
end
end before_action :find_book # class implementation
end
The method before_action first gets defined when included in the class, then gets called while the class is being defined. But on what is before_action being defined? The answer lies in the crypticclass << self on the second line above. class << tells Ruby that you want to open a singleton class. Here we want to open the singleton class of self, which is currently defined as the books controller class.
In Ruby, a singleton class is where an object’s class methods are defined. Consider the following IRB code:
> class A; end
=> nil
> instance = A.new
=> #<A:0x007fe51cc85b78>
> singleton = instance.singleton_class
=> #<Class:#<A:0x007fe51cc85b78>>
> instance.class
=> A
> singleton.class
=> Class
> instance.public_methods(false)
=> []
> singleton.public_methods(false)
=> [:allocate, :new, :superclass]Here we’ve defined a class A and instantiated an instance of the class. Then we call the method singleton_class on instance. Next, we call class on both instance and singleton.
Notice that instance’s class is as expected, A. In other words, instance is an instance of classA. Similarly, singleton’s class is Class. In other words, singleton is an instance of class Class. This makes sense when we look at the public methods on both instance and singleton. instance has no public methods — we didn’t define any in class A. But we did call new on A to create instance, and new is defined on A — an instance of Class.
That may be tricky to follow. Re-read it a few times if you have to. The key here is that everything in Ruby is an object — even a class. And in Ruby, we can define methods on any class.
Rails defines methods on our class’ singleton class. We can use them to DRY up our code, as we did by using the before_action method to DRY up our repetitive calls to find_book.
There’s nothing stopping us from defining our own singleton class methods to help DRY up patterns in our own code.
A Unique Example
In one of our applications at Clutter, I noticed a pattern involving classifying objects as kinds. I saw code like this in a class modeling coupons:
class Coupon < ActiveRecord::Base
module Kind
EMPLOYEE = 'employee referral'.freeze
CUSTOMER = 'customer referral'.freeze
AFFILIATE = 'affiliate referral'.freeze
AMBASSADOR = 'ambassador referral'.freeze
end KINDS = [Kind::EMPLOYEE,
Kind::CUSTOMER,
Kind::AMBASSADOR,
Kind::AFFILIATE] validates :kind, presence: true, inclusion: { in: KINDS } def employee?
kind == Kind::EMPLOYEE
end def customer?
kind == Kind::CUSTOMER
end def ambassador
kind == Kind::AMBASSADOR
end def affiliate
kind == Kind::AFFILIATE
end
end
And in a class modeling events:
class Event < ActiveRecord::Base
module Kind
ARRIVE = 'arrive'.freeze
DEPART = 'depart'.freeze
end KINDS = [Kind::ARRIVE,
Kind::DEPART] validates :kind, presence: true, inclusion: { in: KINDS } def arrive?
kind == Kind::ARRIVE
end def depart?
kind == Kind::DEPART
end
end
And in a class modeling documents:
class Document < ActiveRecord::Base
module Kind
INVOICE = 'invoice'.freeze
RELEASE = 'release'.freeze
end KINDS = [Kind::INVOICE,
Kind::RELEASE] validates :kind, presence: true, inclusion: { in: KINDS } def invoice?
kind == Kind::INVOICE
end def release?
kind == Kind::RELEASE
end
end
It’s easy to recognize the duplication here:
- each class defines a module
Kindwith constants for each kind; - each class defines a constant
KINDS, with an array of kinds; - each class validates that there is an attribute
kind, the value of which is one of the enumerated kinds; and - each class defines helper methods to determine if an instance of the class is of a particular kind.
The tricky part is this duplication is more abstract than the book finding code from the first example. This is less duplication in substance, and more duplication in form. Less concrete, more abstract.
However, even though this duplication is abstract, we can DRY it up using what we know about singleton classes and just a bit of metaprogramming.
Sandi Metz often says, “write the code you wish you had.” In this case, I wish I had this code:
class Coupon < ActiveRecord::Base
kinds :employee, :customer, :affiliate, :ambassador
endclass Event < ActiveRecord::Base
kinds :arrive, :depart
endclass Document < ActiveRecord::Base
kinds :invoice, :release
end
Here, kinds is implemented as a method call, with an array of symbols as its argument. Remember, the above code could also be written like this:
class Coupon < ActiveRecord::Base
kinds(:employee, :customer, :affiliate, :ambassador)
endclass Event < ActiveRecord::Base
kinds(:arrive, :depart)
endclass Document < ActiveRecord::Base
kinds(:invoice, :release)
end
Now I just need a kinds method that, just like the before_action in our controller from above, is defined on the singleton class of each of the model classes.
module KindEnumeration
extend ActiveSupport::Concern included do
def self.kinds(*list)
const_set('Kind', Module.new)
const_set('KINDS', list.map(&:to_s).freeze) list.each do |item|
self::Kind.const_set(item.to_s.upcase, item.to_s.freeze) define_method("#{item}?") do
kind == item.to_s
end
end send(:validates, :kind, presence: true,
inclusion: { in: list.map(&:to_s) })
end
end
end
Here we define a module that extends ActiveSupport::Concern. The model defines a class method kinds that takes an array as argument. (Note that the included block instructs Ruby to put the method definitions on the including class’ singleton class.)
When kinds is called, the following things happen to the caller:
- a module
Kindis defined, with constants for each kind; - a constant
KINDSis defined, with an array of kinds; - a validation is added ensuring that there is an attribute
kind, the value of which is one of the enumerated kinds; and - helper methods are defined to determine if an instance of the class is of a particular kind.
In other words, everything that was duplicative in the application code is now encapsulated in this module.
There may be a few methods that look foreign to you, like const_set, define_method and send. Let’s take these methods at their word and speak outloud what they do.
Consider const_set. The caller calls the method kinds, which in turn calls the method const_set, which in turn sets a constant on the caller. So if Coupon calls the methodkinds, then Coupon will have constant Kind defined on it.
The same is true with define_method. The caller calls the method kinds, which in turn calls the method define_method, which in turn defines an instance method on the caller. So if Coupon calls the method kinds, then Coupon will have an instance method {{item}}? defined on it (where {{item}} is an element from the array passed in to kinds).
Now we just need to make sure that our models have access to the kinds method before they call it.
class Coupon < ActiveRecord::Base
include KindEnumeration kinds(:employee, :customer, :affiliate, :ambassador)
endclass Event < ActiveRecord::Base
include KindEnumeration kinds(:arrive, :depart)
endclass Document < ActiveRecord::Base
include KindEnumeration kinds(:invoice, :release)
end
This will absolutely work. But the call to include KindEnumeration is itself duplicative. What if we wanted to change the name of the module? We’d have to change it in many places. A little more metaprogramming can DRY that last bit up.
We know that include is just a method on our model class’ singleton classes, as was before_action. We also know that each of our model classes inherit from ActiveRecord::Base. So if we can invoke include on ActiveRecord::Base, with theKindEnumeration module as an argument, then any class that inherits from ActiveRecord::Base will have access to our kinds method.
module KindEnumeration
extend ActiveSupport::Concern included do
def self.kinds(*list)
const_set('Kind', Module.new)
const_set('KINDS', list.map(&:to_s).freeze) list.each do |item|
self::Kind.const_set(item.to_s.upcase, item.to_s.freeze) define_method("#{item}?") do
kind == item.to_s
end
end send(:validates, :kind, presence: true,
inclusion: { in: list.map(&:to_s) })
end
end
endActiveRecord::Base.send(:include, KindEnumeration)
That’s it! Now our model classes look like this:
class Coupon < ActiveRecord::Base
kinds :employee, :customer, :affiliate, :ambassador
endclass Event < ActiveRecord::Base
kinds :arrive, :depart
endclass Document < ActiveRecord::Base
kinds :invoice, :release
end
Our code is DRY. And if a new feature request comes in, we only have to implement it in one place. For example, if we want to allow our kind attribute to be nil on all of our models, it’s as easy as going to our KindEnumeration module and updating one line of code:
send(:validates, :kind, presence: true,
inclusion: { in: list.map(&:to_s) },
allow_nil: true)A Conclusion
DRY is a valuable axiom. It reminds us that repetitive code is costly to change. In order to achieve the speed and scale necessary to grow our businesses, it’s incumbant upon us as engineers to keep the cost of changing our code as low as possible.
When we notice duplication in the substance of our code, we can generally extract that code to a method and call it. But when we notice duplication in the form of our code, we can rely on Ruby’s singleton class and a little bit of metaprogramming to help us keep duplication, and therefore cost, to a minimum.
Are you passionate about using cutting-edge technology to solve complex technology problems? We are hiring! Check out Clutter’s careers page or contact me at david.sherline [at] clutter [dot] com to learn more.
