In Progress
Unit 1, Lesson 1
In Progress

Module Builder Pattern – Part 1

Love it or hate it, metaprogramming is an undeniable part of Ruby programming lore and culture. And while you can avoid metaprogramming indefinitely if you want to, once you dive in, it pays to invest in robust techniques. In today’s episode, guest chef Chris Salzberg is your guide to introduce you to one of the most important and foundational patterns for Ruby metaprogramming: the Module Builder pattern. In this first episode of a series, you’ll see how Ruby modules are instances of a class just like anything else… and just like any object, they can be customized. Enjoy!

Video transcript & code

The Module Builder Pattern (Part 1)

In this episode, we're going to look at a powerful technique in Ruby for building configurable modules.


class PaymentsApi
end

To do that, I'd like to start with a little example to motivate our discussion. Imagine that we're working with code that makes payments through an API.


class PaymentsApi
  def make_payment(amount)
    # code to call API
  end
end

Here's the class that calls this payments API. Lately we’ve been realizing we need better instrumentation around the API calls in this class so that we can diagnose production issues.

We've all been there, right? What do you do?


class PaymentsApi
  def make_payment(amount)
    puts "making payment with: #{amount}..."
    # code to call API
  end
end

Well, one way to debug the issue is to stick some logging code into the method, just before the original method body, which prints out what is being passed into the method.

This gives us a bit more insight, but soon we realize that we’re still missing some vital information in our logs.


class ApiResponse
  def initialize(status, json)
    # code to initialize response object
  end
end

On further investigation, we start to think the response object might be at fault for the production errors we’ve been seeing. Let's stick some logging code in there as well.


class ApiResponse
  def initialize(status, json)
    puts "initializing response with: #{status}, #{json}..."
    # code to initialize response object
  end
end

Unfortunately this doesn't tell us enough, so we dig deeper.


class InternalApi
  def store_details(details)
    puts "storing details: #{details}..."
    # ...
  end
end

We try the same thing with another method in another class, and get enough information to diagnose the issues.

But then, the next day, we have a new issue, and the logs aren't telling us enough. So we dive in again, adding yet more logging code to more methods.

This doesn't seem very flexible or scalable. Can we avoid all this duplicated code, so we have a consistent logging interface we can apply to many different methods?


module Loggable

As Rubyists, the first thing we reach for to eliminate code duplication is the module.

Modules in Ruby are a way to wrap up a collection of methods and constants into a reusable component to be included in many classes.


module Loggable
  def make_payment(amount)
  end
end

The logging example happens to be a difficult one to generalize with modules, though. Let's look at why that is.


module Loggable
  def make_payment(amount)
    puts "making payment with: #{amount}..."
    super
  end
end

Let's start by filling in the body of this method. We add a line to log the method call, followed by super to call the original method.


module Loggable
  def make_payment(amount)
    puts "making payment with: #{amount}..."
    super
  end
end

PaymentsApi.prepend(Loggable)

Now we can prepend this module into our class so that it's called before the payment method. (Note that if we used include here, it would never get called since the instance method would override the module method.)


PaymentsApi.new(...).make_payment(100)

Now if we call the method, the logger code will be called first, followed by the method code.


PaymentsApi.new(...).make_payment(100)
#=> making payment with: 100...

And that works: it logs the debugger message.


module Loggable
  def make_payment(amount)
    puts "making payment with: #{amount}..."
    super
  end
end

But is this really reusable? Think about the assumptions this module makes:

  • the method must be named make_payment

  • the logger must be puts

  • the message to be logged must be "making payment with:" followed by the argument


module Loggable
end

This may work for this one particular case, but it's not very flexible, and won't generalize to all the logging issues we may have.

What can we do to improve this?

To answer that question, we're going to make a small digression to think about modules and objects in Ruby.


module Loggable
  # define module methods...
end

Let's recall first that everything in Ruby is an object, and every object is an instance of a class.


module Loggable
  # define module methods...
end

Loggable.class

So this module should also be an instance of a class. What class is that?


module Loggable
  # define module methods...
end

Loggable.class #=> Module

It's the Module class! This is just a class like any other, except that it happens to be a class whose instances are modules.


module Loggable # <- an *instance* of Module
  # define module methods...
end

One of those instances is this module here, Loggable. It's not really obvious in this form that Loggable is an instance of anything, though.

Let's see if we can make this clearer.


module

Look at this "module" keyword here. What's it actually doing anyway?


module 

Well, think of it this way. You give it a module name, like Loggable.


module 
  

And then you define some methods in a block of code, like this. And what do you get back?


module 
  
end #=> defines a module

Well, you get a module with that name, of course. But how does this actually happen? And where does that Module class we saw earlier come in here?


 = Module.new do
  
end

In fact, what is actually happening looks more like this. The module keyword is treated as a call to the new method of the Module class, which invokes initialize on a new module instance.

The block of code is passed as an argument to this initializer. The block is then called inside the module initializer, which is how methods get defined on it.

Finally, the module is returned and assigned to a constant, like "Loggable".


my_new_module = Module.new do
  
end

The fact that the module is assigned to a constant is actually not really important. We usually see modules as constants, but you can actually assign a module to anything, like for example a local variable.

The key point is that modules are just objects. Whether they're assigned to a constant or not doesn't change what they do.


Loggable = Module.new do
  
end

Ok, let's bring this back to our example. The module in that case was assigned to Loggable, a constant.


Loggable = Module.new do
  def make_payment(amount)
    # ..
  end
end

Inside that block we have the method declaration, where we define the method make_payment.


Loggable = Module.new do
  def make_payment(amount) # <- we want to configure this
    # ..
  end
end

Now, to return to the topic of this episode, we want a way to configure this method. More specifically, we want to pass the name of this method as an argument somehow when creating the module.


... = Module.new do

How can we do that? We need a class that creates a module from a method name instead of from a block of code.


... = LoggerBuilder.new(...)

Let's just imagine for a second that we have such a class. Call it LoggerBuilder. This is what it would look like if we had it.


... = LoggerBuilder.new("make_payment")

We initialize it with a method name, and it returns to us a module which defines an override for that method, like the one we saw earlier for make_payment.


make_payment_logger = LoggerBuilder.new("make_payment")

We then assign the result to a variable. As mentioned earlier, this is possible since modules are just objects, and you can assign objects to any variable you like.


make_payment_logger  = LoggerBuilder.new("make_payment")
initialize_logger    = LoggerBuilder.new("initialize")
store_details_logger = LoggerBuilder.new("store_details")

Now we can use the same LoggerBuilder class to define other modules, one for each method we want to log.


make_payment_logger  = LoggerBuilder.new("make_payment")
initialize_logger    = LoggerBuilder.new("initialize")
store_details_logger = LoggerBuilder.new("store_details")

PaymentsApi.prepend(make_payment_logger)
ApiResponse.prepend(initialize_logger)
InternalApi.prepend(store_details_logger)

And then we prepend each class with the corresponding logger module.


PaymentsApi.prepend(LoggerBuilder.new("make_payment"))
ApiResponse.prepend(LoggerBuilder.new("initialize"))
InternalApi.prepend(LoggerBuilder.new("store_details"))

In fact, since we're only using each of these configured modules once, we don't really need variables for them at all. We can just include them directly, like this.

This is starting to look pretty nice and clean.


class LoggerBuilder

Now that we know what we want, how do we get it? How do we define this LoggerBuilder thing so that it works this way?


class LoggerBuilder
  def initialize(method_name)

Well, we know it's going to have to have an initializer that takes the name of the method to be logged. It's got to have that.

What else do we know? Well, it's instances are modules. Or, another way to put it: its instances can be included into classes.

As it turns out, this is a very important property we need. Only one class can do this for us, and we've already seen it.


class LoggerBuilder < Module
  def initialize(method_name)

That's right, it's the Module class! By subclassing this class, we inherit its properties, including the one that we want, namely that its instances can be included into classes.

This is the core of the module builder pattern. What we’re defining is not a new module, but a new kind of Module, which can have its own instances.


class LoggerBuilder < Module
  def initialize(method_name)
    # define module methods

In the next episode, we'll fill in the details of this initializer. And we'll see that module builders can be extended to generalize all kinds of things beyond just method names.

Until then, happy hacking!

Responses