Building a tool that's easy for your team to use

In previous articles I shared how I moved a solution to a problem into a general tool.

Building your own tools helps you avoid solving the same problem over and over again. Not only does it give you more power over the challenges in your system, but it gives you a point of communication about how a problem is solved.

By building tools around your patterns you'll be able to assign a common language to how you understand it's solution. Team members are better able to pass along understanding by using and manipulating the tools of their trade rather than reexplaining a solution and repeating the same workarounds.

We can compress our ideas and solutions into a simpler language by building up the Ruby code that supports it.

Here's the code:

module ProcessLater
  def later(which_method)
    later_class.enqueue(initializer_arguments, 'trigger_method' => which_method)
  end

  private

  def later_class
    self.class.const_get(:Later)
  end

  class Later < Que::Job
    # create the class lever accessor get the related class
    class << self
      attr_accessor :class_to_run
    end

    # create the instance method to access it
    def class_to_run
      self.class.class_to_run
    end

    def run(*args)
      options = args.pop # get the hash passed to enqueue
      self.class_to_run.new(args).send(options['trigger_method'])
    end
  end

  def self.included(klass)
    # create the unnamed class which inherits what we need
    later_class = Class.new(::ProcessLater::Later)

    # name the class we just created
    klass.const_set(:Later, later_class)

    # assign the class_to_run variable to hold a reference
    later_class.class_to_run = klass
  end
end

I showed how I'd use this code with this sample:

class SomeProcess
  include ProcessLater

  def initialize(some_id)
    @initializer_arguments = [some_id]
    @object = User.find(some_id)
  end
  attr_reader :initializer_arguments

  def call
    # perform some long-running action
  end
end

Unfortunately EVERY class that uses ProcessLater will need to implement initializer_arguments. What will happen if you forget to implement it? Errors? Failing background jobs?

Ruby's Comparable library is an example of one that requires a method to be defined in order to be used properly, so it's not an unprecedented idea.

Dangerous combination: implicit dependencies and confusing failures

The Comparable library is a fantastic tool in Ruby's standard library. By defining one method, you gain many other useful methods for comparing and otherwise organizing your objects.

But here's an example of what happens when you don't define that required method:

# in a file called compare.rb
class CompareData
  include Comparable

  def initialize(data)
    @data = data
  end
end

first = CompareData.new('A')
second = CompareData.new('B')

first < second # => compare.rb:12:in `<': comparison of CompareData with CompareData failed (ArgumentError)

comparison of CompareData with CompareData failed (ArgumentError) isn't a helpful error message. It even tells me the problem is in the < data-preserve-html-node="true" method and it's an ArgumentError, but it's actually not really there.

If you're new to using Comparable, this is a surprising result and the message tells you nothing about what to do to fix it.

If you know how to use Comparable, you'd immedately spot the problem in our small class: there's no <=> method (often called the "spaceship operator").

The Comparable library has an implicit dependency on <=> in classes where it is used.

We can fix our code by defining it:

# in a file called compare.rb
class CompareData
  include Comparable

  def initialize(data)
    @data = data
  end
  attr_reader :data

  def <=>(other)
    data <=> other.data
  end
end

first = CompareData.new('A')
second = CompareData.new('B')

first < second # => true

After what could have been a lot of head scratching, we've got our comparable data working. Thanks to our knowledge of that implicit dependency, we got past it quickly.

Built-in dependency warning system

Although it's true that the documentation for Comparable says The class must define the <=> operator, it's always nice to know that the code itself will complain in useful ways when you're using it the wrong way.

Sometimes we like to dive into working with the code to get a feel for how things work. Comparable and libraries like it that have implicit dependencies don't lend themselves to playful interaction to discover it's uses.

I mentioned this implicit dependency in the previous article:

The downside with this is that we have this implicit dependency on the initializer_arguments method. There are ways around that and techniques to use to ensure we do that without failure but for the sake of this article and the goal of creating this generalized library: that'll do.

But really, that won't do. Requiring developers to implement a method to use this ProcessLater library isn't bad, but there should be a very clear error to occur if they do forget.

Documentation can be provided (and it should!) but I want the concrete feedback I get from direct interaction with it. I'd hate to have developers spend time toying with a problem only to remember hours later that they forgot the most important part.

Better yet, I'd like to provide them with a way to ensure that they don't forget.

We could check for the method we need when the module is included:

module ProcessLater
  def self.included(klass)
    unless klass.method_defined?(:initializer_arguments)
      raise "Oops! You need to define `initializer_arguments' to initialize this class in the background."
    end
  end
end

class SomeProcess
  include ProcessLater
end # => RuntimeError: Oops! You need to define `initializer_arguments' to initialize this class in the background.

That's helpful noise. And it should be easy to fix:

class SomeProcess
  include ProcessLater

  def initializer_arguments
    # ...
  end
end # => RuntimeError: Oops! You need to define `initializer_arguments' to initialize this class in the background.

Wait a minute! What happened!?

When the Ruby virtual machine processes this code, it executes from the top to the bottom.

The included hook is fired before the required method is defined.

We could include the library after the method definition:

class SomeProcess
  def initializer_arguments
    # ...
  end
  include ProcessLater
end # => SomeProcess

Although that works, other developers will find this to be a weird way of putting things together. Ruby developers tend to expect modules at the top of the source file. Although this example is small, it is, afterall, just an example so we should expect that a real world file would be much larger than just these few lines. Finding dependecies included at the bottom of the file would be a surprise, or perhaps we might not find them at all when first reading.

Everything in it's right place

Let's keep the included module at the top of the file to prevent confusion and make our dependencies clear.

We can automatically define the initializer_arguments method and return an empty array:

module ProcessLater
  def initializer_arguments; []; end
end

But that would do away with the helpful noise when we forget to set it.

One way to ensure that the values are set is to intercept the object initialization. I've written about managing the initialize method before but here's how it can be done:

module ProcessLater
  def new(*args)
    instance = allocate
    instance.instance_variable_set(:@initializer_arguments, args)
    instance.send(:initialize, *args.flatten)
    instance
  end
end

The new method on a class is a factory which allocates a space in memory for the object, runs initialize on it, then returns the instance. We can change this method to also set the @initializer_arguments variable.

But this also requires that we change the structure of our module.

Because we want to use a class method (new) we need to extend our class with a module instead of including it.

Our ProcessLater module already makes use of the included hook, so we can do what we need there. But first, let's make a module to use under the namespace of ProcessLater.

module ProcessLater
  module Initializer
    def new(*args)
      instance = allocate
      instance.instance_variable_set(:@initializer_arguments, args)
      instance.send(:initialize, *args.flatten)
      instance
    end
  end
end

Next, we can add a line to the included hook to wire up this new feature:

module ProcessLater
  def self.included(klass)
    later_class = Class.new(::ProcessLater::Later)
    klass.const_set(:Later, later_class)

    # extend the klass with our Initializer
    klass.extend(Initializer)

    later_class.class_to_run = klass
  end
end

The final change, is to make sure that all objects which implement this module, have the initializer_arguments method to access the variable that our Initializer sets.

module ProcessLater
  attr_reader :initializer_arguments
end

No longer possible to forget

Our library will now intercept calls to new and store the arguments on the instance allowing them to be passed into our background job.

Developers won't find themselves in a situation where they could forget to store the arguments for the background job.

Here's what it's like to use it:

class SomeProcess
  include ProcessLater

  def initialize(some_id)
    @some_id = some_id
  end
  attr_reader :some_id

  def call
    # ...
  end
end

That's a lot simpler than adding a line in every initialize method to store an implicitly required @initializer_arguments variable.

Although developers on your team will no longer find themselves in a situation to forget something crucial, you may still not like overriding the new method like this. That's might be a valid concern for your team, and I have an alternative approach to create a custom and explicit initialize method next time.

For now, however, we can see that Ruby gives us the power to make our code easy to run in the background, but Ruby gives us what we need to automatically manage our dependencies as well.

What this means for other developers

When we write software, we are not only solving a technical or business problem, but we're introducing potential for our fellow developers to succeed or fail.

This can be an important factor in how your team communicates about your work and the code required to do it.

It may be acceptable to have a library like Comparable which implicitly requires a method to be defined. Or perhaps something like that might fall through the cracks and cause bugs too easily.

If we build tools that implicitly require things, it's useful to automatically provide them.

Ready to go

We finally have a tool that can be passed along to others without much fear that they'll run into surprising errors.

Our ProcessLater library is ready to include in our classes. We can take our long-running processes and isolate them in the background by including our module and using our later method on the instance:

class ComplexCalculation
  include ProcessLater

  # ...existing code for this class omitted...
end

ComplexCalculation.new(with, whatever, arguments).later(method_to_run)

This gives us a way to reevaluate the code which might be slow or otherwise time consuming and make a decision to run it later. As developers come together to discuss application performance issues, we'll have a new tool in our vocabulary of potential techniques to overcome the challenges.

Finally, here's the complete library:

module ProcessLater
  def later(which_method)
    later_class.enqueue(initializer_arguments, 'trigger_method' => which_method)
  end

  attr_reader :initializer_arguments

  private

  def later_class
    self.class.const_get(:Later)
  end

  class Later < Que::Job
    # create the class lever accessor get the related class
    class << self
      attr_accessor :class_to_run
    end

    # create the instance method to access it
    def class_to_run
      self.class.class_to_run
    end

    def run(*args)
      options = args.pop # get the hash passed to enqueue
      self.class_to_run.new(args).send(options['trigger_method'])
    end
  end

  def self.included(klass)
    # create the unnamed class which inherits what we need
    later_class = Class.new(::ProcessLater::Later)

    # name the class we just created
    klass.const_set(:Later, later_class)

    # add the initializer
    klass.extend(Initializer)

    # assign the class_to_run variable to hold a reference
    later_class.class_to_run = klass
  end

  module Initializer
    def new(*args)
      instance = allocate
      instance.instance_variable_set(:@initializer_arguments, args)
      instance.send(:initialize, *args.flatten)
      instance
    end
  end
end

When you solve your application's challenges, how to you build new tools? In what ways are the tools you build aiding future developers in there ability to overcome challenges without confusing errors or unknown dependencies?

Get a FREE sample chapter

Get a sample chapter of Clean Ruby and you'll be added to my periodic newsletter of helpful Ruby tips.