Commands are used in many applications, especially GUIs. Komando is an implementation of the Command pattern, especially suited to Rails applications. Commands are provided the information they need, then told to run. Commands can be marked best effort, or mandatory. Commands can refer to other commands, and ask that the sub-commands be mandatory or best effort.
Most web applications have a lot of before and after hooks that occur when working with objects: sending a welcome email on registration, incrementing or decrementing counter caches, trigger validation on remote web services. When implemented using callbacks, all these occur without the developer knowing about them. A simple change in one area of the code can have a huge impact somewhere else. Inspiration for this came from Unintented Consequences: The Pitfalls of ActiveRecord Callbacks and Crazy, Heretical, and Awesome: The Way I Write Rails Apps.
require "komando/command" require "komando/active_record" class AdUpdateCommand include Komando::Command def initialize(*args) @initiated_at = Time.now.utc # If you must override #initialize, NEVER forget to call super super # Forgetting to call super will result in NoMethodError and such # being raised from your code. end # Lets exceptions through -- nothing is rescued -- callers will have # to handle exceptions themselves. The command's success/failure state # is determined by the fact that no exceptions are raised. The block's # return value is ignored. # # All #mandatory_steps are run within a single database transaction. mandatory_steps do @ad.update_attributes!(@params) @ad.campaign.update_attribute(:active_ad_units_count, @ad.campaign.ad_units.active.count) end # The #transaction block can be used to root your transaction differently. # The default #transaction block simply yields - no transactions will be # processed. The komando-active_record gem will root your transactions # against ActiveRecord::Base#transaction. # # This method is important if you have more than one database connection, # where each model might open transactions against different databases. transaction do AdUnit.transaction do yield end end end class PlacementEventLoggerCommand include Komando::Command mandatory_steps do Event.create!(:event_type => "placement:created", :actor => @actor, :subject => @subject) end end class PlacementCreationCommand include Komando::Command mandatory_steps do @placement = Placement.create!(@params) @placement.campaign.increment!(:active_placements_count, 1) if @placement.active? @placement.campaign.increment!(:scheduled_placements_count, 1) if @placement.scheduled? end # Call #best_effort_step multiple times to declare each individual step # that will be attempted. If a block fails, logging will ensue, and # other blocks will be attempted. # # #best_effort_step can document what it's supposed to do, enabling # better logging. Either pass a String or Symbol, the latter of which will # be #humanized. best_effort_step(:event_generation) do # Note the availability of a class-level method named #run, which simply does the obvious # instantiation and call the instance-level #run. PlacementEventLoggerCommand.run(:actor => @placement.created_by, :subject => @placement) end end # Usage from Rails class AdsController < ApplicationController def update @ad = Ad.find(params[:id]) # Commands accept any number of parameters, as a Hash. Parameters are translated # to instance variables within the Command object itself. command = AdUpdateCommand.new(:ad => @ad, :params => params[:ad]) if command.run then flash[:notice] = "Ad updated" redirect_to @ad else flash.new[:error] = "Ad failed to save" render :action => :edit end end end
- Fork the project.
- Make your feature addition or bug fix.
- Add tests for it. This is important so I don't break it in a future version unintentionally.
- Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
- Send me a pull request. Bonus points for topic branches.
Komando is known to pass it's specifications on the following Ruby implementations (rvm version specifiers):
- jruby-1.5.6 [ x86_64-java ]
- ree-1.8.7-2011.01 [ x86_64 ]
- ruby-1.8.7-p330 [ x86_64 ]
- ruby-1.9.2-p136 [ x86_64 ]
Copyright (c) 2010 François Beausoleil. See LICENSE for details.