Keeping Your Sanity With The Command Design Pattern
May 14, 2008 18:21

On May 13th 2008, I presented "Keeping Your Sanity With The Command Design Pattern" at the Toronto Ruby/Rails Project Night. Here are the slides and what I said.

Actually this is a somewhat touched-up version of the presentation - there were some microphone issues that threw me a bit, and I had made a few last minute changes right before the presentation (never as good an idea as it may seem at the time) that made me miss a few key points. This version should make a bit more sense.

Hi. I'm Andrew Burke. I've been programming for a living since the mid-90s. Over the years I've worked with Lotus Notes, Java, and PHP. I've been doing work in Rails since mid-2005.

After years of Sole Proprietorship freelancing, I recently started my own company, "Shindig" to sell web-based applications, starting with the Store Ordering System, a business application to help retail companies manage their signage. I presented on the Store Ordering System back at the first Ruby/Rails Project night in January, but it was more of a general business-focused presentation. I've had a number of requests to go into more technical detail, particularly on the Command design pattern. So that's what I'll be covering tonight.

First a quick update on Shindig. Since the last presentation in January, I now have business cards - really nice ones - and a website. I've been working on a slick design, but I'm actually growing rather attached to the simple ad-hoc design I started with. It's funny how easy it is to work on other people's websites, but how neurotic one gets in relation to one's own site. There's currently a blog, a mailing list, and some background information - more is coming soon.

I took the month of February to rework the Store Ordering System quite a bit. Anyone who has worked on a large project has probably felt "It would be great if I had a month to rework some of those core pieces that looked like a good idea when I started but aren't so great anymore" - well, since it's my company, I decided to do that.

It also looks like (knock on wood) that the Store Ordering System will be getting its first real customer in the next month or so.

So, I want to talk about the Command design pattern - something that I've used a lot in the Store Ordering System, but which has been a regular feature of most of the larger projects I've built in many languages over the last decade or so. It's proven to be extremely helpful to me, but I haven't seen it discussed much in the Rails world - so I wanted to share the technique with you.

Coming from the Java world, I've spent a fair bit of time with the famous Design Patterns book, commonly known as the "Gang of Four" book. It's one of the few books I've seen outside of a church that has more than one bookmark sown into the binding.

When I moved to Ruby and Rails from Java, I discovered that a lot of the classic patterns weren't actually all that useful anymore. About a third of the patterns in the book help make static languages like C++ and Java more dynamic and polymorphic - which isn't needed so much since Ruby is already so dynamic. You don't need an AbstractWaterfowlFactoryImpl class if your language only cares if the object has a 'quack' method. About another third of the patterns in the book are more suited to complex real-time GUI environments, which is overkill when you're doing static context request-response cycles, like in most web applications. The exception for this would be all the fancy new JavaScript interfaces people are building now, but that's more of a client issue than a server issue.

That said, there are still a few of the original patterns that I've continued to find useful in my Rails development. One of these is a scaled-back and simplified version of the Command pattern.

What is the Command pattern? Basically it's a way of wrapping up a distinct piece of program activity into its own class. The original Gang of Four implementation is fairly complex and supports things like multiple types, stacks of completed commands with state snapshots in them for undo / redo (and you can see this at work in any large commercial GUI application like Word or Photoshop). My version is quite a bit simpler, and is for keeping my projects cleaner and easier to maintain.

So, how does it work and why should you use it?

Well, say you're building a B2B application that manages retail signage programs (it happens, you know). In your system, store managers can place orders for, say, new window decals after their front windows were broken. This order goes to staff at head office who review it and, if everything is fine, click on the "Confirm Order" button.

In a simple Rails application, you'd be tempted to simply put some code into the controller to get the order by the passed key, change the status, save the order, and then redirect elsewhere.

But what happens if the logic is more complicated? For example, you need to make sure that the order needs to have a customerordernumber, and you need to add a timestamp. Now you're getting quite a bit of code and business logic in your controller, which is really about managing the views and redirecting - business logic here is a side effect.

But what do you do when this is a real-world application with lots of different requirements: is there enough of the ordered items at the warehouse? If not, production orders need to be set up. Is this installation or a painting order, without any inventory at all? Is this shipping direct to the store location from the suppliers? Is it a warehouse restocking order? Notification emails need to be sent to a number of people based on various criteria, and at the end of it all a snapshot confirmation printout needs to be made so there's a permanent record of what was ordered.

This is too much code to sneak into part of your controller - so it should go somewhere else. The obvious place would be into a model. But which one? This is mostly related to the Order model, but it's hitting many other parts of the application, including warehouse inventory, email notifications, individual order items, and printouts. Also, since the order model is the heart of the application, it will become gigantic as almost all of the workflow logic is built into it as methods, mixed in with ActiveRecord calls, validations, and delegates.

This is where Commands come in.

My version of the command pattern isn't a full Gang of Four implementation, with factories and stacks and undo methods and the like - but it's a clear separate place to put the actions of your business logic.

It's all about keeping things clear and separate. To make this explicit, I like to set up an extra subdirectory in the 'app/' directory, so we have "models" "views" "controllers" and "commands". To get this new folder to be treated like the other app folders, put this into the 'config/environment.rb' file:

config.load_paths += %W(#{File.dirname(FILE)}/../app/commands )

This way the commands are all loaded into Rails without needing separate requires or includes.

Commands are all classes that are descended from the 'MasterCommand' class. It's pretty simple and consists of two helpful static methods:


class MasterCommand
# given a hash of params and an array of symbols, this makes sure that all of the symbols are keys in the hash.

# If not, it raises an error and logs it in the EventTrack table
def self.params_present?(params, required_symbols)
missing_params = Array.new
required_symbols.each do |next_symbol|
missing_params << ' ' << next_symbol if params[next_symbol].nil? || params[next_symbol].to_s == ''
end

if !(missing_params.empty?)
  Eventtrack.warning(nil, self.name + '.valid_params?', 'Missing data:' + missing_params.to_s)
  raise "Command Missing data:" + missing_params.to_s
end

end

def self.get_by_key_or_object(input_value, expected_class)
if input_value
if input_value.kind_of?(expected_class)
input_value
else
expected_class.find_by_id(input_value)
end
else
nil
end
end
end

Commands take a hash of parameters rather than a comma-separated list. The 'params_present' method checks the passed parameters hash against the passed array of symbols to ensure that required parameters have been included - a simple sanity-check assertion.

The second method 'get_by_key_or_object' allows the calling code to pass either an integer key or an full ActiveRecord object, and this will return the appropriate model - or nil if there's a problem.

I know that there's similar functionality buried somewhere in the Rails code itself, but this only took me ten minutes to write rather than the hours needed to poke through the Rails libraries.

Here's what a simple command looks like (in the presentation I picked a more complex example, but I think it confused the issue - here is a very minimal one instead):

# Checks all job items and marks job stocked if all have been stocked/received class CmdCheckOrderComplete < MasterCommand # this takes the following parameters: # user: the user who is running this command (required) # order: the order that we're checking or its key (required) # Goes through all of the orderitems in this order, and if all of them are completed service items, marks the order as complete. def self.run(params = {}) params_present?(params, [:user, :order]) _order = get_by_key_or_object(params[:order], Order) if _order && _order.is_service_order? all_complete = true _order.orderitems.each do |next_item| all_complete = false if !(next_item.is_completed_service?) end CmdOrderComplete.run(:user => params[:user], :order => _order) if all_complete end end end

This is a simple command that checks the order to see if it has been completed - if it has been, then this calls another command, "CmdOrderComplete". I prefix all of my commands with 'cmd' so it's easy to tell them apart from other code, and so they group together in alphabetical listings. I like to put fairly detailed comments at the top of the command files, which shows up in RDoc and makes it clear what each command is supposed to do and what parameters it takes. As you can see, the run method is static and takes a hash of params, which are checked through the params_present? method. We also load up the order (the name of which I underscore to help clarify that this is a passed parameter). The actual meat of the command is pretty simple.

The previous command was small enough that it could have probably been a method inside the Order model or a controller without making much of a mess - but here's the first two thirds of the 135 lines of code for the command that runs when confirming an order. Even though it's hard to see the actual code in these screenshots, it's easy to see that this isn't the most elegant code ver. This is because it's live real code in one of the more complicated parts of a continuously evolving program - but what's important is that it's encapsulated and contained ugly code. Also, this is everything that happens when confirming an order. To figure out what's going on, I only have to look in this one place.

Here you can see this command being called in the controller. It's in the middle of a longish select statement that handles the many buttons that could be clicked on the main Order page - each with different behaviours, different navigation results, and different command calls. The controller code here is complicated enough just in figuring out what was clicked and where to go next - imagine how crazy it would be if business logic was mixed into here instead of being put into commands.

So that's how Commands work. But what's really useful about this pattern?

First off, Commands map really nicely to how people think about things in the real world. When sketching out an application and working with end users, I figure out the nouns and the verbs of the business situation and how they relate. The Nouns end up becoming ActiveRecord Models, and the Verbs end up becoming Commands. From a list of nouns and verbs, I then set up the basic migrations, models, and commands. Once the models and commands are set up, I can get a good idea of what an application is and what it does just by looking at the 'models/' and 'commands/' directories.

Another great feature of Commands is in testing. Now that everything that the application does is wrapped up into Commands, it's much easier to test the workflow, piece by piece and as a whole.

I put a 'commands' subdirectory inside my 'test/units' directory, which makes the command tests run when I call rake test:units.

Also, since all of the possible workflow activities happen in commands, I can set up full start-to-finish workflow tests: Create an order, add items to it from a catalogue, submit it, reject it, resubmit it, confirm it, set up supplier orders, pack and ship supplier items to fulfillment warehouses, send items to store locations, return items - the whole process can be tested with the real application logic and without lots of complicated and brittle fixtures.

The Store Ordering System has a number of different workflows based on the kinds of orders that are being processed - and each has its own complete workflow test inside a 'workflows' directory under 'test/units' so they also run when I call 'rake test:units'.

Even better, my TestHelper has a number of methods for setting up orders to a particular state in their workflow - so if I'm testing invoicing somewhere in my unit or functional tests, I only have to call the BuildCompletedOrder from the TestHelper and it will give me a real completed order, with all of the correct values set as they would be in the real system and ready for invoicing - not some fixture that I set up six months ago and which doesn't reflect the changes that have been made since then.

The most important advantage of the Command pattern is CLARITY. Rails is already a remarkably clear framework: I often have to parachute into semi-completed applications and it's much easier to figure out what's going on in a Rails application than a Java or PHP application.

However, Commands make it even easier. If someone complains that something is going wrong when they confirm an order, the application is organized like the real world situation - so it's likely that the problem is in the CmdOrderConfirm command.

With Commands, you know where to put code, and where to find it later. Before I worked with Commands, I'd constantly worry about where I should put a given piece of functionality - does it really make sense now? Will it make sense there later? Would other people be able to find it there in the future? Would I?

The Command pattern isn't exactly sexy, but it can really help make an application clearer and more maintainable. And, frankly, what's really sexy is being able to finish early and go have a beer.

Next:
torontorb
May 19, 2008 09:31
Other Blog Posts
Finding Burgers Fast: My DIY Halifax Burger Week Site "This is Nowhere" at PodCamp Halifax 2018 The Diary Diaries: Fixing Remembary's Facebook Connection Special Leap Day Edition of "Some Weird Things About Time" What's Up With Remembary Can't get pg_dump To Work Now That Heroku Has Upgraded Postgresql to 9.4? The Best Thing I Ever Did To Promote My App If You Build It, They WON'T Come #deployaday, My Big Hairy Plan for 2015 Extracting Plain Text from an NSAttributedString My Year of "Hits" Part 2: Remembary Rolling My Year of "Hits" Part 1: Remembary Rises (and Stumbles) Handy Little Test Method to Check for Translations in Rails Apps My Suddenly Slow-Waking MacBook Air Indie App PR: Keeping Control of Your Tone A Quick Note on 'clone' in Rails 3.2 My eBook Apps 2: iOS, JavaScript, and Ruby My eBook Apps 1: Introduction Quick Tip: No Sound on Mountain Lion My Upcoming Talk at PodcampHFX 2012: My Year of "Hits" starshipsstarthere.ca: Building at the Speed of Funny Screencast Tips Remembary's Cool New Picture Support Indie App PR 2: Keeping On Top Of User Feedback Indie App PR 1: How to Handle an App Disaster Giles Bowkett Diary Project 2 Remembary Video Congratulations! Welcome to Your Nightmare! How My iPad App Remembary Took Off Why You Should Have an App in the App Store (Even If You Probably Won't Make Any Money) PodCampHFX Remembary Presentation - Part 3 How I Used MailChimp Autoresponders to Promote Remembary PodCampHFX Remembary Presentation Part 2 PodCampHFX Remembary Presentation Part 1 Why AdWords Ads Don't Work for iPad Apps Remembary is Sponsoring PodcampHFX Why Can't I Resize my Views in Interface Builder? Momento and Remembary Concerning Remembary iPad-Friendly eBooks of Gracian's Art of Worldly Wisdom Project Report: PTOS2 A Quick Note on Encryption We're all LUsers Thoughts on HAML Friday Afternoon Hack - Getting Beyond the Basics Halifax Friday Hack and Back to Basics Quote from Wil Shipley FutureRuby Make Web Not War Busy Week I: Toronto Ruby Job Fair Employment.nil - the Toronto Ruby Job Fair Code Count: Ruby on Rails vs. C#/ASP.NET A Brief Note on Twitter The Hub Halifax and Mobile Tech for Social Change Deep Thoughts on Microsoft From The Accordion Guy The Two Kinds of Defensive Programming Presentation - Fixing Careerious: From C#/.NET to Ruby on Rails Enterprise! Presenting at Ruby on Rails Project Night - May 7th New Name and New Look for Careerious/Clearfit FutureRuby and More From Unspace Health Tips for Programmers This tables meme won't die Careerious - Ruby and Rails vs. C#/.NET Yeah I Use Tables For Layout, So Sue Me The Different Kinds of Done Giles Bowkett's RubyFringe presentation OfficeTime: Great Time-Tracking App for OS X Back With A New Look Non-DRY Feed torontorb Keeping Your Sanity With The Command Design Pattern shindigital Is All Grown Up! (according to the spambots) Startup Stars? I'm so bored! The Magic Words for RMagick Jennifer from Operations You see? Naming is HARD Business Software as Process Documentation Deployment note: 'execve failed' Steve Jobs on Market Research Why Canada Is Better for Entrepreneurs "Program first and blog second" Toronto Tech Collage The MacBook Air Is A Roadster RubyFringe! Quote of the Week: Steve Yegge Starting Up: Cards Great design tool: browsershots.org Starting Up: The Logo Quotes Of The Day: Hedge Fund Interview TSOT Ruby / Rails Presentation Night - Part 1 Moneyworks: Accounting Software for Canadians on OS X Starting Up: The Name Nice logo, but why is your site so bland? Welcome to shindigital.com