Using Wisper to Decompose Applications

Screen Shot 2013-06-18 at 5.39.22 PMWorking on a new project is fun and feels super productive. Features can be added quickly and prototyping ideas leads to a tight feedback loop ideal for Agile/Lean-esque planning.

However, as time goes by, I found that the initial momentum enjoyed early on in the project decreases and doing test-first development of Rails applications becomes difficult. In contrast, when writing libraries a test-first approach seems natural.

The predominate voice in the Rails community advocates the so-called “Rails Way”. This is the golden prescribed path, the reason many of us, no doubt, took to Rails in the first place. It’s the beauty of Rails and its famed productivity boosts.

But somehow these gains seemed frustratingly short lived.

After working on several none-trivial Rails applications, I began to recognise the symptoms and by listening to others I began to piece together the causes of my frustration.

The Inspiration

I first heard of Hexagonal Rails in a talk by Matt Wynne at GoRuGo 2012. I immediately recognised the problems he described in my own code, particalluy controllers written in a procedural style, littered with if statements. Matt shows how to overcome this using the pub-sub pattern, which is what I will be demostrating in this article.

The idea behind Hexagonal Rails is the separation of the core application from the delivery mechanism, something echoed in the keynote talk, “Architecture the Lost Years”, by Robert Martin and “Objects on Rails” by Avdi Grimm.

The take away idea is your app is not a Rails app. Rails is nothing more than a framework used to wrap your app in the HTTP protocol. It’s a detail and we should be concerned with Object-Orientated development, not Rails-orientated development.

Yet Another Ebay Clone

Imagine we are working for a startup that is building an ebay clone for a niche market. On the product pages someone can bid on an item by entering an amount and pressing a “Bid Now” button.

In the beginning, the story is very simple; create a new bid and let the user know if it was successful or not. Easy, use the “Rails Way”, deliver and ship. Agile-ing-hell-fire.

Sometime later, new stories are added to the backlog:

  • Email the seller of the item
  • Update the UI of item watchers in realtime
  • Store a statistic
  • Update the activity feeds for all subscribers of the item

The code for these new stories needs to touch several models and hit an external API. Wherever it goes, (model, observer, or controller) it’s going to introduce dependencies that are not core concerns of “bidding on an item”. Testing and refactoring will become difficult and productivity slows.

Where else could this code go? We already have the words: “create bid” or CreateBid. It sounds like it would be a good candidate for a new object that models the creation of a bid.

This type of object is often refereed to as a service, use case, or command. I will use the word ‘service’.

Services are typically used to orchestrate interactions across multiple models and/or interact with external systems (e.g. HTTP and SMTP).

A service object seems like a good idea, but haven’t we just moved the problem?

Aren’t our services just going to get bloated like out models? Yes!

Wispered Service Objects

Wisper is a library which supplements Ruby’s Methods and Attributes with Events.

We will use it to allow our service objects can publish events to other objects in a decoupled manner.

The service object can perform its primary action and then broadcast the outcome, an event, to any listeners. Listeners are added to the service object within the context in which the service is executed, in this case, the context is the controller. Lets take a look at an example:

The controller:

class BidsController
  def new
    @bid = Bid.new
  end

  def create
    service = CreateBid.new
    service.subscribe(WebsocketListener.new)
    service.subscribe(ActivityListener.new)
    service.subscribe(StatisticsListener.new)
    service.subscribe(IndexingListener.new)
    service.on(:reserve_item_successfull) { |bid| redirect_to item_path(bid.item) }
    service.on(:reserve_item_failed)      { |bid| @bid = Bid.new(bid_params); render :action => 'new' }
    service.execute(current_user, bid_params)
  end

  private

  def bid_params
    params[:bid]
  end
end

The service:

class CreateBid
  include Wisper::Publisher  

  def execute(performer, attributes)
    bid = Bid.new(attributes)
    bid.user = performer
    if bid.valid?
      bid.save
      notify_seller_of(bid)
      broadcast(:bid_successful, performer, bid) 
    else 
      broadcast(:bid_failed, performer, bid)
    end
  end

  private
  
  def notify_seller_of(bid) 
    BidMailer.new_bid(bid).deliver 
  end 
end

Some listeners:

class WebsocketListener
  def bid_successful(performer, bid)
    Pusher.trigger("item_#{bid.item_id}", 'new_bid', :amount => bid.amount)
  end
end

class ActivityListener
  def reserver_item_successful(performer, bid)
   # ...
  end
end

The first thing you might notice is that the controller has no if‘s, is very readable, and has a pleasing shape.

We have moved the auxiliary concerns (websocket notifications, activity feed) in to seperate objects.

The controller (context) does the following:

  • Creates a service object
  • Wires up any listeners that need to know about the response
  • Executes the service passing in data pulled from the HTTP request

Interestingly, CreateBid does not tell listeners to do something, its tells them something happened. This is subtle but important. CreateBid does not know the context in which it is being executed (it might not be a web app) or who is listening. Objects which trust, not control, others only need to make choices based on their own state. This creates a clear boundary between objects and their responsibilities and does not constrain choices we can make in the future about how use our domain objects.

If we need to add a new feature as part of the registration process we have a two choices:

  • Add code to the service object. Appropriate if the feature is intrinsically part of the use case being expressed by the service. In my example I decided sending an email is part of registration process.
  • Add a listener to the service object. Most appropriate if the feature is auxiliary to the use case expressed by the service, should not happen in all circumstances in which the service is executed and/or is specific to the delivery mechanism.

Publishing events

Simply include Wisper::Publisher in any object to allow it to publish events.

 class MyPublisher
 include Wisper::Publisher

  def do_something
    publish(:done_something, 'hello', 'world')
  end
end

The first argument is the event to be broadcast followed by any number of arguments that should be passed to the listeners.

When publish is called done_something will be called on all listeners which have (respond_to?) this method. The method must have an argument signature which can accept the arguments being published, so in the above example a listener should have:

def done_something(greeting, location)
  # ...
end

The publish method is also aliased as broadcast and emit.

Subscribing to events

my_publisher = MyPublisher.new
my_publisher.subscribe(MyListener.new)

We can also optionally constrain which events are sent to a listener:

create_bid.subscribe(WebsocketListener.new, :on => :bid_successful)

You can also pass an array to on if you need to specific multiple events.

If the listener has a method which does not match the name of the event being broadcast you can map it a different method name using with.

create_bid.subscribe(BidListener.new, on => :bid_successful, :with => :successful)

We can also subscribe a block, this is used to great effect in controllers to allow them to subscribe to events and respond in the correct way. You could reasonably add the controller as a listener, e.g. create_bid.subscribe(self), but adding a block makes it look more Rails-ish.

create_bid.on(:reserve_item_successfull) do |reservation|
  redirect_to reservation_path(reservation)
end

Global Listeners

Wisper also allows adding of “global listeners” which are automatically subscribed to every publisher.

In a Rails application I would typically add global listeners in an initalizer.

Wisper::GlobalListeners.add_listener(StatisticsListener.new)

This can be convenient but this creates indirection. The execution path can be less obvious. I would suggest using global listeners with caution. You will also, like regular Rails observers, need to turn them off in your tests.

In Summary

By combining Service objects with Wisper we get the following benefits:

  • Smaller objects with fewer responsibilities
  • Objects can be wired up based on context in a lightweight manner
  • The core application is not polluted with the delivery mechanism concerns
  • Objects tell listeners what happened, not what to do (trusting, not controlling)
  • Isolated testing is easier and because we can do away with requiring Rails tests run faster.
  • Clear boundaries exist between objects in different layers.
  • Moving to async is easier since we can execute from a background job without Rails loaded

Easier to test, means refactoring is easier, which means longevity increases, resulting in higher value for money and greater profit.

As with any such consideration, one size does not fit all. Personally I don’t use Wisper with simple CRUD apps. But if (when!) the CRUD app develops beyond the Rails sweet spot then I start using Wispered service objects.

It’s very easy to apply Wisper to an existing codebase. You can start with one controller or model.

Refactoring mature codebases

Luckily this concept is very easy to apply to a mature code base in stages, partcularly if you have good test coverage. Simply choose somewhere to start, like a bloated model or a crazy if laced controller.

As I developed Wisper, I tried many techniques for implementing Pub-Sub in my code and it still contains some of those experiments. I only replace them with Wisper when I need to touch the controller in question for some other reason.

Once a model or service object has Wisper in place, adding new listeners is very easy. For example, caching can be moved out of the model/observer/controller and in to a listener.

You can even apply Wisper to an ActiveRecord model as easily as any other object if you wish to remove some after_ callbacks.

Have a go, find one place in your Rails app that needs some TLC and create a branch. See if it works out for the better.

I’d love to hear how you get on, your thoughts and feedback on Github, in the comments or on Twitter.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://archlever.blogspot.com/ Jeff Dickey

    Great stuff! One thing I’d point out to people who are rushing off to redo their controllers using this is: be cognisant of your tools! As an obvious example, there are thousands of apps out there that use tools like Devise for authentication. They’ll have some interesting challenges trying to move authentication out into a domain object because Devise reopens your app’s ApplicationController and also requires modifications to your User model that are hard to encapsulate.

    Life as a developer is a continuous series of choices between expedience and wisdom. Rails, and much of its ecosystem, have made a huge chunk of that choice “for” you.

    • http://teamcoding.com Kris Leech

      @Jeff, Thanks for commenting. I totally agree. Devise is one example which is tricky to refactor in to this style. Its much easier if use something which offers better control like `has_secure_password` or `omniauth`. I use Devise on a number of apps which use Service objects, I’m happy to have the two workflows side by side, it seems practical to do so.

  • http://blog.firsthand.ca Nicholas Henry

    Hello Kris, Thank you for writing this article and the gem. I have been interested in this style of programming since reading the GOOS book and enjoy seeing others take on it; especially in the context of Rails. Some feedback on your article:

    * BidsController line # 11 Typo: :reserve_item_successfull should be :reserve_item_successful
    * The service appears to be broadcasting incorrect messages – :bid_successful and bid_failed while the BidsController and WebsocketListener are listening to :reserve_item_successful (typo corrected) and :reserve_item_failed. (unless I’m completely missing something)