Module Builder pattern in Ruby
Tags: ruby, builder, pattern
Published: 2023-09-25 13:37:00

---
title: Module Builder pattern in Ruby
published: true
description: Modify and specify modules at mixing time
tags: Ruby, builder, pattern
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
published_at: 2023-09-25 13:37 +0000
---

## What is Module Builder pattern?

It's a nifty little technique where instead of including a static, predefined module, we include/extend/prepend a module that is returned by some method, the builder call:

```rb
# static include
include MyModule

# built module include
include MyModuleBuilder.call(args)
```

The benefit of introducing this one step of indirection is that now we have a place to customise the module, and document and enforce the contract between the module and the including structure.

## Eliminate secret API

Existing articles (see below) do a great job explaining how using a module builder gives flexibility to what module is built and included, but for me the unsung winner feature is that it allows specifying otherwise implicit relationships between the module and the includer. Or as [Chris Salzberg put it](https://dejimata.com/2017/5/20/the-ruby-module-builder-pattern)
> Encapsulate this in the place where it best belongs: in the module itself.

Codebases I've been working with have been littered with code like this:

```rb
module EmailInvites  
  def send_invite
    EmailSender.call(email: self.email, content: content, delay: DELAY)
  end
end

class User < ApplicationRecord
  DELAY = 5.minutes

  include EmailInvites  
end
```

Yes, we spec `#send_invite` (usually with a shared example, shudder) and it works, but we've completely omitted crucial details about the relationship that are necessary for easy maintenance and extension of existing logic, namely:
1. We haven't specified anywhere that the including class must implement `#email`, and that it's expected to be a string value that will be used as recipient email
2. We haven't specified that the module depends on a constant `DELAY` being defined

Using a Module Builder can help make these requirements explicit and self-documenting. Let's specify what the module needs in the inclusion call:

```rb
class User < ApplicationRecord
  DELAY = 5.minutes

  include EmailInvitesModuleBuilder.call(for: "User", email_getter: :email, delivery_delay: DELAY)  
end
```

Would be great to verify that User does indeed implement `#email`, but as far as I know, this can only be achieved if the module inclusion is done after the method definition, which goes counter to class content order recommendations. YMMW.

Now for the builder definition. Admittedly, it becomes more complex, because now there are more code scopes to keep track of, but let's see how we get on.

```rb
module EmailInvitesModuleBuilder
  # @param email_getter [Symbol] which method to call on includer instances to obtain email
  # @return [Module]
  def self.call(includer_name:, email_getter:, delivery_delay:)
    # doing some at-mix-in validating
    if includer_name.first.upcase != includer_name.first
      raise ArgumentError.new(":includer_name '#{includer_name}' must be titlecased")
    end

    mod = const_set("#{includer_name}EmailInvites", Module.new)

    # fill module
    mod.module_eval do
      define_method(:send_invite) do |content|
        {email: send(email_getter), content: content, delay: delivery_delay}
      end
    end

    # explicitly return it
    mod
  end
end
```

And now the often missing part, how to spec this. Follow [David Chelimsky's advice](http://blog.davidchelimsky.net/blog/2010/11/07/specifying-mixins-with-shared-example-groups-in-rspec-2/
)

> We're interested in specifying two fundamentally different things:
>
> * the behaviour of M in response to being mixed-in
> * the behaviour of each class/object that mixes in M in response to events triggered by their consumers

```rb
describe EmailInvitesModuleBuilder do
  describe ".call(includer_name:, email_getter:, delivery_delay:)" do
    subject(:invite_module) { described_class.call(**options) }

    let(:options) { {includer_name: includer_name, email_getter: email_getter, delivery_delay: 5} }
    let(:email_getter) { :address }

    describe "at-mix-in behaviors" do
      context "when an invalid :includer_name is provided" do
        let(:includer_name) { "user" }

        it "raises a descriptive argument error" do
          expect { invite_module }.to raise_error(ArgumentError, ":includer_name 'user' must be titlecased")
        end
      end
    end

    describe "runtime behaviors granted by including built module" do
      let(:includer) do
        Class.new do
          include EmailInvitesModuleBuilder.call(includer_name: "User", email_getter: :address, delivery_delay: 5)

          def address
            "a@a.com"
          end
        end
      end

      it "defines correctly configured #send_invite" do
        expect(includer.new.send_invite("yay")).to eq(email: "a@a.com", content: "yay", delay: 5)
      end
    end
  end
end
```

For modules intended for use in `extend` and that grant "macros" - class methods that will further configure things - specs will have to go one step deeper and show various different ways extending classes could be calling the macro(s), and what effects that will have on instances.  

### Further reading

* Michael Kohl's ["Configurable Ruby Modules: The Module Builder Pattern"](https://blog.appsignal.com/2019/11/29/configurable-ruby-modules-the-module-builder-pattern.html)
* Chris Salzberg's ["The Ruby Module Builder Pattern"](https://dejimata.com/2017/5/20/the-ruby-module-builder-pattern)