A Real World Metaprogramming Case Study

Metaprogramming is one of the unique features of Ruby. Metaprogramming is an interesting concept, and a tool can save a lot of time. It can also be confusing for developers and difficult to maintain if not used properly. Here is an example of how I used Metaprogramming to solve a real-world problem and save a few hours coding.

Serializers With Duplicate Code

I’m working on an application that used for managing vacation bookings. The application needs an API to integrate with some business partners. As part of the specification, dates should be returned in the API in a “YYYY-MM-DD” format.

To build the API, I’ll be using ActiveRecord::Serializer to customize the JSON output.

Here’s a snippet from the reservation serializer class:


class ReservationSerializer << ActiveModel::Serializer
   attributes :starts_on, :ends_on, :posted_on, :reserved_on

   def starts_on
     object.starts_on.strftime("%F")
   end
   def ends_on
     object.ends_on.strftime("%F")
   end
   def posted_on
     object.posted_on.strftime("%F")
   end
   def reserved_on
     object.reserved_on.strftime("%F")
   end
 end

This method works because you can override any attribute in a serializer class. The problem is that this is going to be a lot of duplicate code. There are also several other API endpoints that need to deal with dates.

Wouldn’t be nice if we could format dates without writing a bunch of duplicate methods?

Creating a Super Class That Can Create Methods

In Ruby, parent classes have the ability to modify its children. We are going to use this ability to define methods on our serializers without writing them by hand.

We’ll start by creating an ApplicationSerializer from which each of out serializers will inherit.

class ApplicationSerializer << ActiveModel::Serializer
 end

Inside of this superclass, we are going to define a formatted_date class method that we will use in our subclasses.

Here’s what the final result looks like:

  def self.formatted_date(options = {})
     attribute = options[:attribute] || nil
     format = options[:format] || "%F"

     if attribute.present?
       define_method(attribute) do
         object[attribute].strftime(format)
       end
     end
   end

Let’s break this down:

  • First, we pass an options hash that will take the attribute and the format we will use. We are defaulting to the YYYY-MM-DD format, but this gives other developers the option to override that if needed.
  • Then, We check that an attribute is passed, and if so, we call the define_method method. We will use this to create methods in our subclass. Note that the created method’s name will be the same as the attribute.
  • Finally, We pass a block to define_method. This block will become the body of the new method. Note that the block uses the object property, which belongs to the subclass and not ApplicationSerializer.

Creating Our Dynamic Methods

Inside of our ReservationSerializer class, we change the super class to ApplicationSerializer. Changing the parent class gives us the added ability to create new date attribute methods on the fly. Now writing this:

  formatted_date :attribute => :starts_on

is exactly the same as writing this:

 def starts_on
   object.starts_on.strftime("%F")
 end

Our Updated Class

Let’s take a look at our new ReservationSerializer:

 class ReservationSerializer < ApplicationSerializer
   attributes :starts_on, :ends_on, :posted_on, :reserved_on

   formatted_date :attribute => :starts_on
   formatted_date :attribute => :ends_on
   formatted_date :attribute => :posted_on
   formatted_date :attribute => :reserved_on
 end

Not only is this cleaner to read and less duplicate code, but we have this functionality available in all of our serializers.

[content_upgrade cu_id=”43″]Miss your last deadline? Download a Free Chapter of “Dependable: Deliver Software on Time and within Budget” [content_upgrade_button]Estimate Smarter[/content_upgrade_button][/content_upgrade]
Click Here to Leave a Comment Below 0 comments

Leave a Reply: