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