Brandon Rice

Software development with a focus on web technologies.

Ruby Class Method Mixins

| Comments

Adding custom class methods is a common Ruby-ism that’s used in many gems. Maybe you’ve used state_machine to do something like this:

1
2
3
4
5
class MyMachine
  state_machine :state do
    # other stuff
  end
end

Fairly straight-forward. Plenty of libraries use class methods like this. You can easily group related methods together in your own modules, and then have them defined on the class when the module is later included. This is accomplished with the Module#included method like so:

1
2
3
4
5
6
7
8
9
10
11
module MyModule
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def class_do_something(arg)
      # stuff
    end
  end
end

Then, include this module in one of your other classes:

1
2
3
4
5
class MyClass
  include MyModule

  class_do_something :foo
end

A friend of mine has been working on a Ruby MUD server that I’ve been helping with when time permits. One thing we want to do is persist game objects to disk using any number of backends (Mongo, SQLite, etc.). We want an API that’s storage-type agnostic which can talk to the appropriate mechanism by way of an adapter. The adapter should be hidden from the main application. We want to be able to write object classes that might look like this:

1
2
3
4
5
6
class Weapon
  include Persistable

  field :damage, :float
  field :weight, :int
end

This is very similar to the Mongoid syntax (a gem my friend and I are both fans of). Using the above pattern in our Persistable module allows us to accomplish exactly what we’re going for. The full code after our initial refactor is below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module Persistable
  attr_reader :_id

  def _id
    _id ||= SecureRandom.uuid
  end

  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def self.extended(base)
      base.instance_eval do
        class << self; attr_reader :fields; end
        @fields = []
      end
    end

    def field(name)
      attr_accessor name
      @fields << name
    end

    def read_only_field(name)
      attr_reader name
      @fields << name
    end
  end

end

If you enjoyed this post, please consider subscribing.

Comments