In Progress
Unit 1, Lesson 1
In Progress

Macro

Video transcript & code

In episode 180, I introduced the concept of lazy loading with "ghost objects". In the Episode class, it's possible that not all the data about an episode will be loaded at first. One such attribute is the #description. I've overridden the getter method for #description to first attempt to fully load the episode before returning the episode description text.

module RubyTapas
  class Episode

    attr_accessor :video,
                  :id,
                  :number,
                  :name,
                  :description,
                  :synopsis,
                  :video_url,
                  :publish_time,
                  :load_state,
                  :data_source


    def initialize(attributes={})
      attributes.each do |key, value|
        public_send("#{key}=", value)
      end
      @load_state = :ghost
    end

    def to_s
      inspect
    end

    def ==(other)
      other.is_a?(Episode) && other.id == id
    end

    def published?(time_now=Time.now)
      publish_time <= time_now
    end

    def load
      return if load_state == :loaded
      data_source.load(self)
    end

    def description
      load
      @description
    end
  end

end

description isn't the only field that needs to be lazy loaded, though. Some others that may need to be lazily loaded are the synopsis and publish_time fields. And there may be more in the future. Let's see about adding lazy accessors for these other attributes.

We could simply copy and paste the work I did for description. But this is tedious, and error-prone, and it creates duplication which may bite us if we ever have to change the pattern for lazy loading. Instead, let's make a macro for this.

Of course, Ruby doesn't have actual macros, but we tend to use the term "macro" for any method that gets called at class or module load time, and which generates methods, classes, or modules for us. In this case, we're going to be generating methods.

We'll call our macro #lazy_accessor. Just like Ruby's built in attr_accessor macro, we'll enable it to receive an arbitrary number of attribute names as arguments. For each name, it will first declare a writer method using the standard Ruby attr_writer macro. Then it will declare a custom reader method by calling define_method. The method name is the same as the attribute name. define_method takes a block which will become the body of the new method. Our reader method takes no arguments. It calls #load to force the object to be fully loaded before continuing. Then it returns the hopefully loaded value of the corresponding instance variable using instance_variable_get.

Now we can remove the attr_accessor definitions for our lazy-loaded attributes and instead put them after a call to lazy_accessor. We can also remove our hand-coded definition of the description reader method.

module RubyTapas
  class Episode

    def self.lazy_accessor(*names)
      names.each do |name|
        attr_writer name
        define_method(name) do
          load
          instance_variable_get("@#{name}")
        end
      end
    end

    attr_accessor :video,
                  :id,
                  :number,
                  :name,
                  :video_url,
                  :load_state,
                  :data_source

    lazy_accessor :description,
                  :synopsis,
                  :publish_time


    def initialize(attributes={})
      attributes.each do |key, value|
        public_send("#{key}=", value)
      end
      @load_state = :ghost
    end

    def to_s
      inspect
    end

    def ==(other)
      other.is_a?(Episode) && other.id == id
    end

    def published?(time_now=Time.now)
      publish_time <= time_now
    end

    def load
      return if load_state == :loaded
      data_source.load(self)
    end
  end  
end

Let's assure ourselves that this works. We define a tiny fake data source which will fill in an episode with some hardcoded values.

Then we create an Episode and interrogate it for its lazy-loaded attributes. When we execute this code we can see that, first, the methods exist, and second, that they reflect the lazily-loaded values.

require "./episode.rb"

class << (mapper = Object.new)
  def load(episode)
    episode.description = "Using macros to DRY up repetetive method definitions."
    episode.synopsis    = "Fun with metaprogramming"
    episode.publish_time = Time.now
  end
end

ep = RubyTapas::Episode.new(data_source: mapper)
ep.description                  # => "Using macros to DRY up repetetive method definitions."
ep.synopsis                     # => "Fun with metaprogramming"
ep.publish_time                 # => 2014-01-26 18:06:09 -0500

I haven't mentioned the word "metaprogramming" up until this point, but that's exactly what we've been doing. One of the questions that has come up a few times in RubyTapas episode comments is "when to metaprogram". The code we just wrote demonstrates one answer to that question, which is: use metaprogramming when it can eliminate duplication in ways other techniques can't.

I use the technique shown here pretty often. Anytime I find myself writing the same type of method over and over again in a particular class, I'll often write a little macro to generate those methods instead. If it reduces duplication and it's still clear from the name what will be generated by the macro calls, I think it's worth the added complexity of metaprogramming. In this case, the term lazy_accessor pretty clearly and declaratively expresses what kind of methods will be generated.

In the next episode, we'll look at how to extract this functionality out so it can be shared with other classes. Until then, happy hacking!

Responses