The letter A styled as Alchemists logo. lchemists
Published May 1, 2024 Updated May 3, 2024
Cover
Module Builder Pattern

The Ruby Module Builder Pattern allows you to encapsulate customizable behavior — and store state — which can be applied when extending, including, and/or prepending existing objects (i.e. classes or modules). This solves situations where you need a module to be configurable which is not possible by default.

To illustrate, the following provides the Ruby object hierarchy for modules and classes:

Cover

Notice that modules and classes are siblings to each other while also children of Object (via Kernel). Normally, a Module is not a superclass of Class but I added a green dashed line that shows what the hierarchy looks like when a Class is taught to inherit from Module. Adding this single line makes this pattern possible and unleashes a ton of potential. The rest of this article will focus on unraveling the power behind the green line that makes this pattern possible.

Quick Start

To get started, we’ll experiment with a primitive implementation of the Pipeable gem for composing a series of steps which can be run in sequence. Below is a Ruby Bundler Inline script you can experiment with locally:

#! /usr/bin/env ruby
# frozen_string_literal: true

# Save as `demo`, then `chmod 755 demo`, and run as `./demo`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "amazing_print"
  gem "debug"
end

require "logger"

LOGGER = Logger.new STDOUT, formatter: -> level, _at, _id, message { "#{level}: #{message}\n" }

class Builder < Module
  def initialize *steps
    super()
    define_method(:run) { steps.each(&:call) }
  end
end

class Demo
  include Builder.new(proc { puts "Step I." }, proc { puts "Step II." })

  def initialize logger: LOGGER
    @logger = logger
  end

  def call
    logger.info { "Running steps..." }
    run
    logger.info { "Finished!" }
  end

  private

  attr_reader :logger
end

Demo.new.call

When you run the above you’ll end up with the following output:

INFO: Running steps...
Step I.
Step II.
INFO: Finished!

In this pattern’s most basic form, you create a class (i.e. Builder) that subclasses Module in order to customize inherited behavior. The power lies in the ability to customize behavior based on the context in which the object is included. In the above example, we create an instance of Builder — which is both a module and a class — to supply steps (procs) which will be executed via the #run method.

Basics

We can break the above down further by walking through the basics.

Construction

To implement this pattern, as shown in the example, you must subclass Module and supply parameters. Otherwise, don’t use this pattern if you don’t subclass or define parameters because this pattern will be wasted effort. To elaborate further, here’s the same example constructor, shown earlier, with code comments:

# Subclass `Module` to enable the Module Builder Pattern.
class Builder < Module
  # Customize parameters. In this case, we allow any number of steps.
  def initialize *steps
    # Message `super` without arguments since `Module` doesn't need them.
    super()

    # Dynamically create a `run` instance method which iterates all steps.
    define_method(:run) { steps.each(&:call) }
  end
end

You must always supply super when subclassing so super() is used to ensure no arguments are passed up to the superclass since Module.new only accepts a block and we are not using a block in this situation.

With Module#define_method, we dynamically create a #run method at runtime on the receiver. The receiver is our Builder module which will apply all methods to the class it’s inherited into (i.e. Demo).

Customization

Returning to our earlier example, you’ll notice we were able to customize Builder when included in our Demo class. Here’s the specific code snippet:

class Demo
  include Builder.new(proc { puts "Step I." }, proc { puts "Step II." })
end

Normally, as with any module, we’d only use include Builder but since we are using the Module Builder Pattern, we first initialize Builder with an array of procs which further enhances the reusability of our Builder implementation because each object Builder is included in can customize the steps as desired without losing the core functionality of the Builder implementation. I’ll elaborate on this further in the Advanced section shortly.

Encapsulation

Full encapsulation is provided by default when using this pattern because our Demo object only has access to the steps via the #run method as dynamically added by Builder upon inclusion. Example:

builder = Builder.new
demo = Demo.new

builder.steps  # eval error: private method `steps' called for #<Builder>
builder.run    # eval error: undefined method `run' for #<Builder>
demo.steps     # eval error: undefined method `steps' for an instance of Demo.
demo.run       # "Step I.\nStep II.\n"

Ancestry

The ancestry of each object is descriptive as well:

Builder.ancestors  # [Builder, Module, Object, Kernel, BasicObject]
Demo.ancestors     # [Demo, #<Builder:0x0000000137d1ed80>, Object, Kernel, BasicObject]

In the case of Builder, we see Builder is a descendant of Module as expected. As for Demo, we see Demo is a descendant of an instance of Builder (hence the 0x0000000137d1ed80 memory address). Normally, when using multiple inheritance, you’d only see the module name Builder without the instance’s memory address. This is another indication of the Module Builder Pattern in action.

Advanced

With the basics out of the way, let’s discuss the more advanced aspects of this pattern.

Defining Methods

As shown in our initial implementation, we get immediate benefit from using Module#define_method since this method is available within the context of a Module or Class. We then use define_method to define a new method at runtime on the receiver which is the object that includes Builder.

By default, methods defined at runtime via Module#define_method are public. This is why demo.run is accessible and outputs:

Step I.
Step II.

In general, this is not what you want since tighter encapsulation is better. To fix, we can use the private #included hook. Here’s an updated version of the original implementation:

class Builder < Module
  def initialize *steps
    super()
    define_method(:run) { steps.each(&:call) }
  end

  def included descendant
    super
    descendant.class_eval "private :run", __FILE__, __LINE__
  end
end

Notice we use the private Module#included method hook which triggers when using .include in our Demo implementation (in this case, included is a private instance method instead of a class method but I’ll explain more shortly). Then we use .class_eval to ensure #run is private. Now when we message #run, we get a proper exception:

class Demo
  include Builder.new
end

Demo.new.run
# demo:36:in `<main>': private method `run' called for an instance of Demo (NoMethodError)

Now that #run is private, we can use #call to wrap #run for full encapsulation.

Anonymity

Another variant of this pattern is to use a module singleton method which dynamically builds an anonymous module for inclusion. Here’s what the implementation looks like if we refactor our earlier implementation:

module Builder
  def self.with(*steps)
    Module.new { define_method(:run) { steps.each(&:call) } }
  end
end

class Demo
  include Builder.with(proc { puts "Step I." }, proc { puts "Step II." })
end

This is identical — in terms of functionality — to the original implementation but suffers several problems:

  1. Once you add multiple singleton methods to a single module, you end up with a junk drawer of semi-related functionality. You get better encapsulation when subclassing Module when building your implementation.

  2. The debugability of your ancestry is diminished. To illustrate, using Demo.ancestors yields:

[Demo, #<Module:0x0000000141df9110>, Object, Kernel, BasicObject]

See the problem? Yep, the anonymous Module:0x0000000141df9110 is what we get when using Module.new. We can fix this by refactoring the implementation to set a temporary name:

module Builder
  def self.with(*steps)
    Module.new { define_method(:run) { steps.each(&:call) } }
          .set_temporary_name("builder")
  end
end

class Demo
  include Builder.with(proc { puts "Step I." }, proc { puts "Step II." })
end

Demo.ancestors
# [Demo, builder, Object, Kernel, BasicObject]

Notice builder shows up in the ancestry instead of the anonymous module from earlier. At least now we have a clue as to where the ancestor came from but creating multiple anonymous modules this way gets tedious fast.

I would not recommend this approach if you can avoid it.

Hooks

There is a lot you can do with module hooks. I won’t cover them all but do want to highlight a few scenarios that might be of interest.

Basics

When using this pattern, the normal class level Module hooks are instance hooks instead. To illustrate, here’s how you’d normally implement custom hooks for a standard module:

module Builder
  def self.extended(descendant) = puts "Extended: #{descendant}"

  def self.included(descendant) = puts "Included: #{descendant}"

  def self.prepended(ancestor) = puts "Prepended: #{ancestor}"
end

Except, when using the Module Builder Pattern, you need to use instance methods instead of class methods due to inheriting from Module like so:

class Builder < Module
  def extended(descendant) = puts "Extended: #{descendant}"

  def included(descendant) = puts "Included: #{descendant}"

  def prepended(ancestor) = puts "Prepended: #{ancestor}"
end

The above is easy to forget so remember to drop self when using this pattern or you’ll find none of your hooks working properly.

Disablement

When using hooks, you can prevent undesired behavior. Here’s an example, used in the Containable gem, which ensures you can’t create containers out of classes:

class Builder < Module
  def extended descendant
    fail TypeError, "Only a module can be a container." if descendant.is_a? Class
    super
  end
end

The above does a type check for Class types and raises a TypeError if detected. Generally, you don’t want to implement code that checks types but can be handy in specific situations.

Multiple

Depending on your implementation needs, there is nothing stopping you from using a primary hook to handle additional hooks in a single operation. Here’s an example which leverages the included hook to include and prepend additional modules at the same time:

class Builder < Module
  def included descendant
    super
    descendant.include Comparable
    descendant.prepend Freezable
  end
end

Without knowing what is implemented in the Comparable and Freezable modules, the object ancestry — assuming we included the above into the Demo class — would be as follows:

puts Demo.ancestors

# [
#   Freezable,
#   Demo,
#   Comparable,
#   Builder:0x000000012c48fdc8,
#   Kernel
#   BasicObject
# ]

As you can see, Freezable is prepended while Comparable and Builder are included afterwards. There is a trade off with this approach as you are adding more objects to the ancestry but can be worth it if the Comparable and Freezable implementations are complex or reused elsewhere in your implementation. That said, strive to keep your ancestry to a minimum.

Syntactic Sugar

To improve this pattern further, a bit of syntactic sugar can help by using brackets to enhance the Object API. Example:

class Builder < Module
  def self.[](*) = new(*)
end

In this case, single splat forwarding is used but double splats (i.e. **) or even full argument forwarding (i.e. ...) would work depending on your needs (see my Ruby Method Parameters And Arguments article for further details). This allows us to use include Builder[step_a, step_b] instead of include Builder.new(step_a, step_b). The use of .[] is more self describing of the data type desired instead of using .new. Both work, but I find .[] to be more elegant. With the above modification, this means you can now do this:

class Demo
  include Builder[proc { puts "Step I." }, proc { puts "Step II." }]
end

Much better! Ideally, you’d clean this up further so all of your steps are predefined elsewhere in your application for further reuse. Example:

STEP_A = proc { puts "Step I." }
STEP_B = proc { puts "Step II." }

class Demo
  include Builder[STEP_A, STEP_B]
end

You’re not limited to using an array. Hashes work too. Granted, a hash is not appropriate for our current Builder implementation but we can look at the Containable and Infusible gems since they both implement the Module Builder Pattern while taking arrays or hashes for arguments. Example:

require "containable"

# Creates a new container with a custom registry.
module Container
  extend Containable[register: CustomRegister]

  register :one, 1
  register :two, 2
end

require "infusible"

# Sets up the import by infusing the above container.
Import = Infusible[Container]

class Demo
  # Includes our `:one` and `:two` dependencies only that `:two` is aliased as `:other`.
  include Import[:one, other: :two]
end

Both the Containable and Infusible gems use .[] for arrays and hashes instead of .new. Using .[] provides a more succinct and elegant syntax which further amplifies the power of this pattern.

Freezing

You can freeze your implementation of this pattern during initialization like so:

class Builder < Module
  def initialize *steps
    super()
    define_method(:run) { steps.each(&:call) }
    freeze
  end
end

Freezing your implementation is a good performance optimization, so ensure you do this.

Gemification

At a certain point, you might want to extract your implementation as a gem for maximum reusability. Using a tool like Gemsmith speeds up this extraction process. Assuming you are using Gemsmith, your gem skeleton is built, and you’ve named your project Pipeable, you’ll want to provide a convenient interface. Given our existing Builder implementation, we can wrap Builder as follows:

module Pipeable
  def self.included(descendant) = descendant.include Builder.new

  def self.[](*) = Builder.new(*)
end

Notice I’ve provided two, top-level, singleton methods. The first (i.e. .included) ensures we can include Pipeable in any object without arguments. The second (i.e. .[]), provides an elegant way to supply an array of custom steps. Here’s how this works in practice:

# With default steps or no steps at all.
class Demo
  include Pipeable
end

# With custom steps.
class Demo
  include Pipeable[step_a, step_b]
end

If we hadn’t made use of .included, we’d be forced to include Pipeable[] in the first example which is ugly and unnecessary when a safe default exists. Having .[] gives us the ability to provide custom steps if desired. Both of these top-level singleton methods are necessary to provide an elegant design to your gem/library when you want default and custom behavior that isn’t awkward and feels natural.

💡 Keep in mind, you don’t have to use both .included and .[]. Depending on your implementation, you might only need one or the other but not both and that’s fine too. Also, when providing a top-level module, you don’t have to support Builder.[] — as mentioned earlier — since this top-level wrapper handles that for you.

Memory/Performance

In terms of memory consumption and/or CPU performance, the only time this comes into play is when the class is loaded. This is a one-time cost only. There is no memory/performance hit when creating instances since they are created after the class is loaded. So you can create as many instances as you want without additional cost or side effects from using this pattern.

As with standard multiple inheritance, nothing is garbage collected. Unless you are loading a large amount of objects into memory when the class loaded, you won’t see a memory spike or memory being retained. Same goes for CPU performance. Unless you are doing heavy computation upon loading your class, you won’t see a spike in CPU activity either.

Guidelines

When naming objects using this pattern, stick with nouns and adjectives:

  • Nouns: Use names that end in er or or like Builder used in the example above. Nouns are what you use when subclassing Module.

  • Adjectives: Use names that end in able like Observable, Comparable, Forwardable as found within the Ruby core libraries. Adjectives are what you use when providing the common interface to your implementation that upstream applications can use. This is especially handy when extracting as a library or gem.

Complete Example

The following is a refactor of our earlier Ruby Bundler Inline script — as shown in the Quick Start section at the beginning of this article — with many of the advanced learnings applied:

#! /usr/bin/env ruby
# frozen_string_literal: true

# Save as `demo`, then `chmod 755 demo`, and run as `./demo`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "amazing_print"
  gem "debug"
end

require "logger"

LOGGER = Logger.new STDOUT, formatter: -> level, _at, _id, message { "#{level}: #{message}\n" }

class Builder < Module
  def initialize *steps
    super()
    define_method(:run) { steps.each(&:call) }
    freeze
  end

  def included descendant
    super
    descendant.class_eval "private :run", __FILE__, __LINE__
  end
end

module Pipeable
  def self.included(descendant) = descendant.include Builder.new

  def self.[](*) = Builder.new(*)
end

STEP_A = proc { puts "Step I." }
STEP_B = proc { puts "Step II." }

class Demo
  include Pipeable[STEP_A, STEP_B]

  def initialize logger: LOGGER
    @logger = logger
  end

  def call
    logger.info { "Running steps..." }
    run
    logger.info { "Finished!" }
  end

  private

  attr_reader :logger
end

Demo.new.call

With the above you can compare the difference between the original script and this updated script or enhance/experiment further as you like.

💡 Don’t forget, you can use the Pipeable gem if you want a fully functional version of this implementation too.

Additional Examples

The following gems use the Module Builder Pattern is you’d like to explore further:

  • Containable: A thread-safe dependency injection container.

  • Infusible: An automatic dependency injector.

  • Pipeable: A domain specific language for building functionally composable steps.

  • Wholable: A whole value object enabler.

The above are advanced examples of everything we’ve discussed in this article.

History

In case you are curious about the long tail of discussion related to this pattern, the following highlights the previous writings of others despite functionality for this pattern existing since the creation of the Ruby language.

⚠️ Regarding Salzberg’s article, please avoid using anonymous Struct inheritance which is strongly discouraged by the Ruby Core Team. I also write about avoiding this practice in my Ruby Structs and Ruby Data articles. Additionally, use of #send (or more appropriately #__send__) to privately send a message breaks encapsulation. Only use #public_send for dynamic messaging unless within the same object.

⚠️ Regarding Nyh’s article, please avoid using Active Support concerns as suggested in the article. Stick with native Ruby modules because this gives you greater latitude to extract functionality into a library, gem, etc. without being dependent on Rails.

Conclusion

Like any pattern, there are pros and cons. Don’t reach for this pattern if you don’t have anything to configure since a standard module is fine. However, if you do find yourself needing to extract common functionality which can be configured, then this pattern is perfect.

I hope you’ve enjoyed this deep dive in to the Module Builder Pattern. Thanks for reading!