mixology: the art of mixing and unmixing

what

a gem that allows objects to effectively mix and unmix modules

who

Pat Farley, anonymous z, Dan Manges, Clint Bishop

why

modifying an object's behavior by mixing in modules is very useful. thankfully, Ruby gives us this ability out of the box. but sometimes we want to unmix behavior. however, mixing out a module is a bit clumsy in Ruby.

enter Mixology.

let's consider the interface for a door. it's pretty simple. doors basically have two states: open and closed. we knock on doors that are closed so someone will let us in. and if a door is open, we walk right on in.

we might model this like so:

class Door

  def initialize(open = false)
    if open
      extend Open
    else
      extend Closed
    end
    
    def closed?
      kind_of? Closed
    end
    
    def opened?
      kind_of? Open
    end
    
  end
  
  module Closed
    def knock
      puts "knock, knock"
    end

    def open
      extend Open
    end
  end
  
  module Open
    def knock
      raise "cannot knock on an open door, just come on in"
    end
    
    def close
      extend Closed
    end
  end
end

let's write a few tests for our door states.

class DoorTest < Test::Unit::TestCase
  def test_an_open_door_is_opened_and_not_closed
    door = Door.new :open
    assert door.opened?
    assert !door.closed?
  end

  def test_a_closed_door_is_closed_and_not_opened
    door = Door.new
    assert door.closed?
    assert !door.opened?
  end

  def test_closing_an_open_door_makes_the_door_closed_but_not_opened
    door = Door.new :open
    door.close
    assert door.closed?
    assert !door.opened?
  end

  def test_opening_a_closed_door_makes_the_door_opened_but_not_closed
    door = Door.new
    door.open
    assert door.opened?
    assert !door.closed?
  end
end

time to run the tests:

Loaded suite door
Started
..FF
Finished in 0.006623 seconds.

  1) Failure:
test_closing_an_open_door_makes_the_door_closed_but_not_opened(DoorTest) [door.rb:67]:
 is not true.

  2) Failure:
test_opening_a_closed_door_makes_the_door_opened_but_not_closed(DoorTest) [door.rb:74]:
 is not true.

4 tests, 8 assertions, 2 failures, 0 errors

when we instantiate a door and mixin the opened or closed state, our tests pass. however, the tests for transitioning between states fail. when we extend the door instance with the mixin for the new state, the old mixin is still there. this gives us some undesired behavior. although ruby will use the behavior in the most recently included mixin, this can still be problematic. we could change this example to work better for these tests, but then we would still have problems transitioning from open to closed and back.

unfortunately, plain ol' ordinary ruby, or POOR, (Farleyism) does not give us a clean way to implement our state pattern this way. but Mixology does.

here's the code using Mixology:

require "rubygems"
require "mixology"
class Door
  def initialize(open = false)
    @open = open
    if open
      mixin Open
    else
      mixin Closed
    end
  end
  
  module Closed
    ...
    def open
      unmix Closed
      mixin Open
    end
    ...
  end
  
  module Open
    ...
    def close
      unmix Open
      mixin Closed
    end
    ...
  end
end

and now our tests should pass.

Loaded suite door
Started
....
Finished in 0.000793 seconds.

4 tests, 8 assertions, 0 failures, 0 errors

ah, much better.

why not just use a more traditional state pattern approach?

because it's kind of a pain. essentially, traditional, and even non-traditional rubyish, state pattern implementations are laborious.

what else do we use Mixology for?

it's also handy for decorating objects that hang around in memory and need undecorating.

a word on implementation

when we (Clint, Dan, and Nonymous) started implementing mixology, we ran into a few problems with POOR. nonymous called Farley and asked him to take a look at our failing tests. as usual, we had started with failing tests. anyway, Farley had the next day off from work and in one day implemented much of Mixology all by his own self. the rest of us were, of course, busy toiling at our day jobs and contributing to society. so our sincere thanks to you, Farles.

oh, and speaking of Farley's hard work, a JRuby implementation is also available.

getting mixology

you can get Mixology 10 Proof (the latest version) via  gem install mixology

Posted on September 08, 2007