I recently had a discussion about using static methods in ruby as convenience methods to implement the builder pattern.

I have a strong personal preference for favoring dependency injection with defaults via the #initialize method when working with collaborator objects. This frequently gives me class definitions that look like this.

class EmailMessage
  def message(content)
    # ...
  end
end

class Messenger
  def initialize(transport: EmailMessage.new)
    self.transport = transport
  end
  
  def message(message_content)
    formatted_message = format_message message_content
    transport.message formatted_message
  end
  
  private
  
  attr_accessor :transport
  
  def format_message(message_content)
    # ...
  end
end

Which we then use with the default parameters like this.

messenger = Messenger.new
messenger.message "Hello, World!"

This gives us the ability to easily swap out message transport implementations with something else.

class SlackMessage
  def message(content)
    # ...
  end
end

messenger = Messenger.new transport: SlackMessage.new
messenger.message "Hello, Slack!"

We can also use this with test implementations for unit testing the Messenger class.

require "minitest/mock"

mock = Minitest::Mock.new
mock.expect :message, nil, ["Hello, Test!"]

messenger = Messenger.new transport: mock
messenger.message "Hello, Test!"
mock.verify

This leaves us with a very common two line repeating pattern where we instantiate our object, call a method, and then never use the object again.

messenger = Messenger.new transport: SlackMessage.new
messenger.message "Hello, Slack!"

To simplify this I will commonly add static builder methods on the class which does the object instantiation, calls the method, and discards the return value.

class Messenger
  def self.message(message_content, **args)
    new(**args).message(message_content)
  end
  
  # ...
end

Which reduces our common usage to a nice one liner.

# Using default transport
Messenger.message "Hello, World!"

# Using alternative transport
Messenger.message "Hello, Slack!", transport: SlackMessage.new

This feels a lot cleaner to me, however there are a few considerations to keep in mind.

First, this tightly couples the implementation of the ::message method with the #message method. I am generally comfortable with this coupling as it exists within a single class definition and changes to the signature of the #message method can be easily reflected in the ::message method.

I will also intentionally name the static methods and it’s arguments exactly the same as the instance method to make following the code easier. This pattern does introduce a new layer of indirection and I want to make that as easy to follow as possible.