Skip to main content

By clicking Submit, you agree to the developerWorks terms of use.

The first time you sign into developerWorks, a profile is created for you. Select information in your profile (name, country/region, and company) is displayed to the public and will accompany any content you post. You may update your IBM account at any time.

All information submitted is secure.

  • Close [x]

The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerworks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

By clicking Submit, you agree to the developerWorks terms of use.

All information submitted is secure.

  • Close [x]

Crossing borders: Extensions in Rails

The anatomy of an acts_as plug-in

Bruce Tate
Bruce Tate is a father, mountain biker, and kayaker in Austin, Texas. He's the author of three best-selling Java books, including the Jolt winner Better, Faster, Lighter Java. He recently released From Java to Ruby and Rails: Up and Running. He spent 13 years at IBM and later formed the RapidRed consultancy, where he specialized in lightweight development strategies and architectures based on Ruby, and in the Ruby on Rails framework. He is now the CTO of WellGood LLC, a company that is forming a marketplace for nonprofit organizations and charitable giving.

Summary:  The Java™ programming language has long been a great melting pot, with rich and powerful capabilities for integration -- from dependency-injection containers for integrating enterprise libraries, to Enterprise JavaBeans (EJB) technology, to the component models for Eclipse. With so many ideas and architectures available, Java developers pioneer new ways to weave disparate software libraries and components into a cohesive whole. But Java developers don't have a monopoly on good integration techniques. See how Ruby on Rails plug-ins work by looking at a popular plug-in called acts_as_state_machine.

View more content in this series

Date:  13 Mar 2007
Level:  Intermediate
Also available in:   Russian  Japanese

Activity:  21124 views
Comments:  

As I write this article, Texas and Oklahoma are thawing out from a long ice storm. Drivers, fearing not just the ice but other brash Texans driving on it, are beginning to emerge again. My life is approaching some semblance of normalcy after three days of hibernation. I experienced a brief freeze of a different kind after switching from the Java language to Ruby. When I worked with Java projects, I could always find that special Spring library or Eclipse component to solve some niche problem. When Ruby on Rails was new, I'd often need to write it myself. Happily, that freeze too is beginning to thaw nicely, thanks to an effective plug-in architecture that thousands of people have used to extend Rails.

About this series

In the Crossing borders series, author Bruce Tate advances the notion that today's Java programmers are well served by learning other approaches and languages. The programming landscape has changed since Java technology was the obvious best choice for all development projects. Other frameworks are shaping the way Java frameworks are built, and the concepts you learn from other languages can inform your Java programming. The Python (or Ruby, or Smalltalk, or ... fill in the blank) code you write can change the way that you approach Java coding.

This series introduces you to programming concepts and techniques that are radically different from, but also directly applicable to, Java development. In some cases, you'll need to integrate the technology to take advantage of it. In others, you'll be able to apply the concepts directly. The individual tool isn't as important as the idea that other languages and frameworks can influence developers, frameworks, and even fundamental approaches in the Java community.

If you've spent any time at all with Rails, you've doubtless noticed the acts_as commands in ActiveRecord. Though ActiveRecord deals with persistence, you often want to add behavior to your classes beyond database storage and retrieval. For example, by using acts_as_tree, you can add tree-like behavior to a class with a parent_id attribute. By saying nothing more than acts_as_tree in your ActiveRecord model, you can dynamically add methods to manage the tree, such as methods that retrieve the parent record or child record. Over the past month, I've been able to find Rails plug-ins to handle voting, versioning, Ajax, composite keys, and all manner of features that basic Rails doesn't support.

Extension models in Rails, which build on top of features in the Ruby language, look dramatically different from those in the Java language. In this article, I crack open acts_as plug-ins so you can see the extension model from the inside. I provide a partial production example rather than build a toy end-to-end scenario to cover more ground and give you the flavor of real plug-ins and how they're used in real production code.

The state machine API

As many of you know, a state machine is a mathematical expression of a system's state. A state machine has a mixture of nodes representing states and transitions between them. At any given time, a state machine has one active state, also called a current state. Events trigger transitions between states. To illustrate the concept, I'll show an example from my current day job: development and maintenance of ChangingThePresent.org (CTP), a marketplace for nonprofits and donors (see Resources). CTP lets nonprofits submit information about their organizations and a series of gifts -- such as one hour of a cancer researcher's time or books for one student -- letting donors use a simple shopping cart to make a charitable contribution as a gift in another's name. Collecting all of that information leads to logistical problems, so I chose to simplify the workflow with a state machine.

To solve this problem, I use a third-party plug-in, written by Scott Barron, called acts_as_state_machine (see Resources). Like many Rails plug-ins, acts_as_state_machine uses a combination of Ruby capabilities and exclusive Rails features to provide not just a library, but also a domain-specific language (DSL), providing a nice experience for users.

A customer submits content to CTP (submitted state). A CTP administrator then receives the content possibly to edit (processing state). If CTP makes edits, the nonprofit should be able to approve those changes (nonprofit_reviewing state). When CTP or the nonprofit approves the content, CTP can show the content on the site (accepted state). Figure 1 is a pictorial representation of the state machine:


Figure 1. State machine for CTP
State machine for CTP

Using the plug-in, I can decorate my class object directly, using a DSL to represent the various states, transitions between them, and the events that fire those transitions. Listing 1 shows a simplified version of the state machine that I use to manage nonprofits at CTP:


Listing 1: Example state machine

class Nonprofit < ActiveRecord::Base

  acts_as_state_machine :initial => :created, :column => 'status'
  
  # These are all of the states for the existing system. 
  state :submitted            
  state :processing           
  state :nonprofit_reviewing  
  state :accepted             
  
  event :accept do
    transitions :from => :processing, :to => :accepted
    transitions :from => :nonprofit_reviewing, :to => :accepted
  end
  
  event :receive do
    transitions :from => :submitted, :to => :processing
  end

  # either a CTP  or nonprofit user edits the entry, requiring a review
  event :send_for_review do   
    transitions :from => :processing, :to => :nonprofit_reviewing
    transitions :from => :nonprofit_reviewing, :to => :processing
    transitions :from => :accepted, :to => :nonprofit_reviewing
  end  

You might not have seen all of these Ruby features before, but the language describing the state machine flows nicely. You see descriptions of each of the states, followed by the events supported by the state machine. After each event, you see a series of transitions that each event will fire.

Each statement represents valid Ruby syntax. After the class definition, you see acts_as_state_machine :initial => :created, :column => 'status'. As a Java developer, you may find it strange to find a method invocation instead of a method definition. Ruby refers to these method invocations at the class level as macros. Ruby often uses macros to add capabilities to each class as it's loaded. In fact, method definitions -- def -- are no more than Ruby macros.

Next, you see a series of states, such as state :submitted. These are method invocations, each taking a symbol as a single parameter. (A symbol is a user-defined name.) The event command is also a method invocation, taking a symbol (which defines the event's name) and a closure, which defines the transitions.

Each transition is a method invocation followed by a hash table. In Ruby, you represent a hash map as key => value pairs, separated by commas, and {enclosed in braces}. When you use a hash map as a function call's last parameter, the braces are optional. You can see that the methods -- state, transition, and event -- combined with closures and hash maps, make a nice DSL.

To use the state machine, I can instantiate a Nonprofit object and call methods on it for each event, followed by a !, as in Listing 2:


Listing 2. Manipulating the state machine

>> np = Nonprofit.find(2)
=> ...
>> np.current_state
=> :submitted
>> np.receive!                              
=> true
>> np.accept!
=> true
>> np.current_state
=> :accepted

The ! is a Rails convention for methods that modify and save an attribute in one step. So the requirements for the state machine plug-in are clear. I need:

  • A convenient place to put the state machine code.
  • A way to specify my class methods, which are required for the DSL.
  • A way to attach my instance methods to Nonprofit or any other target class.

The rest of this article walks you through the plug-in. If you want to pull down the code and follow along, download the acts_as_state_machine plug-in. (See Resources for a link to Scott Barron's site and follow his directions for getting the plug-in through Subversion.) Navigate to trunk/lib. You'll find the acts_as_state_machine.rb file. Find the initialization code in trunk/init.rb. These are the only two files you need.


Acts_as plug-ins

In principle, all acts_as plug-ins work the same way. You always follow these steps to build an acts_as module:

  1. Create a module. Begin the name of a class method (your initialization macro) with acts_as_.
  2. In some initialization code, open the ActiveRecord base class and add your acts_as_ module.
  3. Extend the behavior of the target class in an acts_as_ function (for example, acts_as_state_machine).

Take a quick look at the initialization code in init.rb, shown in Listing 3:


Listing 3. Initialization code for acts_as_state_machine

require 'acts_as_state_machine'

ActiveRecord::Base.class_eval do
  include ScottBarron::Acts::StateMachine
end


This code opens the core ActiveRecord class (ActiveRecord::Base) and adds acts_as_state_machine. The class_eval method opens the class and runs the following closure in the context of the class. Whew. That's a mouthful. In practice, the concept is simple: the code opens up the ActiveRecord base class and mixes in the ScottBarron::Acts::StateMachine module. In Ruby, you can open up any class and redefine it quickly.

This capability is one of Ruby's greatest strengths because of the increased flexibility. But the capability is a weakness too. Too much flexibility can lead to code that's difficult to understand and maintain, so be careful. Now, open the acts_as_state_machine.rb file to see what code gets mixed in.


Initializing the module

At this point, I'm going to steer away from the details of implementing the state machine. Instead, I concentrate on exposing the interface to the state machine through the plug-in. Listing 4 shows the module definition and some of the interface of the state machine itself:


Listing 4. The module structure

module Acts                        #:nodoc:
  module StateMachine              #:nodoc:
    class InvalidState < Exception #:nodoc:
    end
    class NoInitialState < Exception #:nodoc:
    end
    
    def self.included(base)        #:nodoc:
      base.extend ActMacro
    end
    
    module SupportingClasses
      class State
        attr_reader :name
      
        def initialize
          ...
        end
        
        def entering
          ...
        end
        
        ...
      end
      
      class StateTransition
        attr_reader :from, :to, :opts
        
        def initialize
          ...
        end
        
        def perform
          ...
        end
        ...
      end
      class Event
      ...
        def fire
          ...
        end
        
        def transitions
          ...
        end
        ...
      end

At the top of Listing 4, you see a nested module definition. A module has method definitions, but no base inheritance hierarchy. Instead, you can attach modules to any existing Ruby class. If the concept is new to you, think of a module as an interface plus the implementation for that interface. The nice thing about a module is that you can attach its functionality to any existing Ruby class, and you can attach as many as you want. You can also leverage a class's existing capabilities. This technique is called mixing in. C++ uses multiple inheritance to provide a similar capability, but with ugly complications. The Java founders eliminated multiple inheritance to address those complications. With modules, you can get some of the benefits of multiple inheritance without the sticky complications. Languages such as Smalltalk and Python also support mix-in inheritance.

The rest of Listing 4 shows some of the mundane details that go into implementing the state machine. You just need to know that these classes provide a stand-alone implementation of a state machine. The rest of the code is far more interesting because it deals with exposing that state machine interface to the plug-in's clients.


Acts_as modules

Recall that a plug-in author needs three things: a place to put the implementation, a way to expose the DSL (the class methods), and a way to expose the instance methods for the state machine. These include the event methods that you saw in action in Listing 3. Listing 4 provided the place to put the implementation. The next slice of code handles the DSL.

The acts_as plug-in architecture has one anchor point: the acts_as macro. Clients of the acts_as plug-in introduce this method with a method invocation in the target class. In my case, I invoke the acts_as in Listing 1 from the Nonprofit class with this line of code:

acts_as_state_machine :initial => :created, :column => 'status'

Now take a look a Listing 5, which provides the ActMacro for acts_as_state_machine. This class handles the attributes for the module and introduces the various class and instance methods.


Listing 5. Adding acts_as

module ActMacro
  # Configuration options are
  #
  # * +column+ - specifies the column name to use for keeping the state (default: state)
  # * +initial+ - specifies an initial state for newly created objects (required)
  def acts_as_state_machine(opts)
    self.extend(ClassMethods)
    raise NoInitialState unless opts[:initial]
    
    write_inheritable_attribute :states, {}
    write_inheritable_attribute :initial_state, opts[:initial]
    write_inheritable_attribute :transition_table, {}
    write_inheritable_attribute :event_table, {}
    write_inheritable_attribute :state_column, opts[:column] || 'state'
    
    class_inheritable_reader    :initial_state
    class_inheritable_reader    :state_column
    class_inheritable_reader    :transition_table
    class_inheritable_reader    :event_table
    
    self.send(:include, ScottBarron::Acts::StateMachine::InstanceMethods)

    before_create               :set_initial_state
    after_create                :run_initial_state_actions
  end
end

The module in Listing 5 has a single method: acts_as_state_machine. The method does five tasks:

  • Introduces class methods
  • Handles state machine exceptions
  • Manages attributes
  • Introduces instance methods
  • Handles before and after filters

The acts_as_state_machine method first introduces class methods. (You can see a precise listing of these methods in Listing 6.) Next, the method handles exceptions. In this case, the only exception occurs when the client doesn't specify an initial state. Skip the inheritable attributes briefly -- I dive into those next. The self.send method introduces instance methods. (Listing 7 shows those in detail.) Finally, the before and after filters are ActiveRecord macros that call the set_initial_state and run_initial_state_actions before and after ActiveRecord creates a record.

Go back to the write_inheritable_attribute and class_inheritable_reader macros. You may be wondering why the module doesn't use simple inheritance. The reason is simple: The module has an inheritance hierarchy of its own. These macros allow the module to project these attributes onto the target class -- Nonprofit in this example. The most important attributes are state_column and a series of transition tables containing the states, events, and transitions. Now it's time to add the class methods that form the DSL.


Adding class and instance methods

In Listing 6, you can finally see the magic introducing the DSL:


Listing 6. Class methods for acts_as_state_machine

module ClassMethods
  def states
    read_inheritable_attribute(:states).keys
  end
  
  def event(event, opts={}, &block)
    tt = read_inheritable_attribute(:transition_table)
    
    et = read_inheritable_attribute(:event_table)
    e = et[event.to_sym] = SupportingClasses::Event.new(event, opts, tt, &block)
    define_method("#{event.to_s}!") { e.fire(self) }
  end
  
  def state(name, opts={})
    state = SupportingClasses::State.new(name.to_sym, opts)
    read_inheritable_attribute(:states)[name.to_sym] = state
  
    define_method("#{state.name}?") { current_state == state.name }
  end
  ...

The event and state macros, as promised, are simple methods, defined in a module called ClassMethods. The event method reads the transition table attribute, followed by the event table attribute. The method adds the event to the event table and then dynamically defines a method for the event, wiring the new method to the fire method on event.

After the event method, the module defines the state method. This method reads the state table and adds the new state. Then, it adds a convenience method to the target class, returning true if the instance is in the current state. For example, nonprofit.submitted? would return true if the status flag were submitted. Now, the DSL is completely supported.

Instance methods work exactly like class methods. Listing 7 shows the instance method:


Listing 7. Instance methods for acts_as_state_machine

module InstanceMethods
  def set_initial_state
    write_attribute self.class.state_column, self.class.initial_state.to_s
  end

  ...
  
  def current_state
    self.send(self.class.state_column).to_sym
  end
  
  ...
end

ActMacro opens the class and adds them. There's no need to go through the read_inheritable_attribute macro to use attributes because these are class instance variables defined by ActiveRecord. I show only the methods that set the initial state and return the current state. The rest work in the same way.

The first method in Listing 7 sets the initial state, updating an existing ActiveRecord column. Recall that I set the column's name when I invoked ActMacro. The current_state method simply returns the instance variable's value. The send method invokes the method named by a single symbol parameter, which in this case is the name of the state_column.


Wrapping up

You might think it would be simpler just to build a state machine and use it as a library. The acts_as plugin is much nicer. It lets you effectively add a state-machine column to your database. Other plug-ins let you do versioning, build in audit histories, handle images, and perform hundreds of other simple tasks, just as if those tasks were a seamless integration between the Rails environment and the database.

You may have used the Java language to integrate Eclipse plug-ins, Ant tasks, or Spring libraries into your code base or to introduce EJB components. Many ideas from the Java community changed the way that developers think about extension. This whirlwind tour of Rails acts_as plug-ins shows a new way to think about it. The flexibility of the Ruby language has changed my thinking about extensions. The acts_as plug-ins allow a new generation of developers to try their hand at writing extensions. The result is a new wave of extensions for Rails. Many of these techniques are available to Java developers too, through aspect-oriented programming or bytecode enhancement.

Next time, I'll wrap up the series with an in-depth comparison between solving a difficult problem using Ruby and my experiences on the Java platform. Until then, keep crossing borders.


Resources

Learn

  • Java To Ruby: Things Every Manager Should Know (Pragmatic Bookshelf, 2006): The author's book about when and where it makes sense to make a switch from Java programming to Ruby on Rails, and how to make it.

  • Beyond Java (O'Reilly, 2005): The author's book about the Java language's rise and plateau and the technologies that could challenge the Java platform in some niches.

  • Plugins in Ruby on Rails: Check out the documentation for the Rails plug-in architecture.

  • acts_as_state_machine: The Rails plug-in that allows an ActiveRecord model to function as a state machine.

  • Changing The Present: The nonprofit marketplace, built in Ruby on Rails, that provided the example for this article.

  • "Class and instance variables in Ruby" (John Nunemaker, RailsTips.org, November 2006): Dealing with class and instance variables, when you're possibly dealing with multiple inheritance, can be tricky. This article walks you through an example of a technique called inheritable attributes.

  • The Java technology zone: Hundreds of articles about every aspect of Java programming.

Discuss

About the author

Bruce Tate

Bruce Tate is a father, mountain biker, and kayaker in Austin, Texas. He's the author of three best-selling Java books, including the Jolt winner Better, Faster, Lighter Java. He recently released From Java to Ruby and Rails: Up and Running. He spent 13 years at IBM and later formed the RapidRed consultancy, where he specialized in lightweight development strategies and architectures based on Ruby, and in the Ruby on Rails framework. He is now the CTO of WellGood LLC, a company that is forming a marketplace for nonprofit organizations and charitable giving.

Report abuse help

Report abuse

Thank you. This entry has been flagged for moderator attention.


Report abuse help

Report abuse

Report abuse submission failed. Please try again later.


developerWorks: Sign in


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Select information in your profile (name, country/region, and company) is displayed to the public and will accompany any content you post. You may update your IBM account at any time.

Choose your display name

The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


Rate this article

Comments

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology, Web development
ArticleID=201831
ArticleTitle=Crossing borders: Extensions in Rails
publish-date=03132007
author1-email=bruce.tate@j2life.com
author1-email-cc=bruce.tate@j2life.com