Tell, don't ask

I've come up with an idea of how to organise my code and it makes so much sense to me I'm wondering why no-one else is doing it. Am I crazy, or is this my legacy contribution to the world of programming?

I wrote about How to reduce the number of support requests you have to deal with. Which comes down to this: include an "application log" in your system - a log of actions that have taken place, in a format that your end users can understand. That way, when they have a question, they (or you) can quickly check the log and see if the explanation is in there - without your developers having to dive into the detailed system logs, or hunt through the code.

Couple that with my "design by nouns and verbs" process - and, in theory at least, you have a framework that displays information, using language that your users are familiar with, which explains the behaviour of the application.

My plan, in Standard Procedure, is to implement this using "Command" objects. As well as representing the "nouns" in your system as database records, each Command represents a verb, again stored in the database as a record. This provides a searchable history of things that have happened. The trouble is, in Collabor8Online, my day job, this is likely to be a huge hassle to implement. The volume of existing data in there makes that strategy very hard to convert over - and I don't really want to have to maintain a "legacy" audit trail alongside a new one. So instead, I'm expanding out my previous system of using an "activities" table that maintains an audit trail of the actions taken. The problem here is that I initially implemented this using ActiveRecord callbacks - and it's grown to be a bit of a mess.

In object-oriented programming, the mantra is "tell, don't ask". The point of OO is that your objects encapsulate their state, keeping it hidden away from prying eyes. That way, there are fewer dependencies and you're free to change the implementation, as long as the public interface (what Smalltalk used to call the "protocol") remains unchanged. So, instead of "asking" your object for information and then acting independently on that data, you "tell" your object to perform the action.

Side note: protocol is exactly the right word for it. Many people dislike OO without realising that they use the same principles each and every day. When you send a HTTP web request - whether that's a GET in a browser or a POST via a JSON API, you're using that same encapsulation technique. You form a message that conforms to the public protocol, send it to the provider and receive a response that also conforms to the protocol. You don't care how it's implemented - it could be Nginx or Apache, it could be HAProxy or it could be a static file-server - all those details are hidden from you. And the provider is free to change their internal implementation and the state it uses to build the response (for example, from static files to using a database) - without you ever knowing. That's OO in action.

Rails - or rather ActiveRecord - is pretty bad at this. ActiveRecord essentially provides a Create-Read-Update-Delete interface to your database, publishing the database fields as attributes. It then mixes in your business logic (the methods) in with those attributes (the state), so it's available to anyone. I'm sure DHH would say that it's actually your system as a whole that provides the encapsulation - with your routes file being the protocol - but it does mean that ActiveRecord isn't really very OO.

To get back to the point, my idea is this:

I needed to ensure that whenever a user of the system does something, the action is recorded in the activities table. Then I realised that the user is telling C8O, or some small piece of C8O, to do something.

So I've written a module called User::Actions that is mixed in to the User object. And it has one really important method: tells. Each controller action that does something (create, update and destroy) should use tells to make it happen.

class FolderMovements < UiController 

  def new 
    authorize! :move_to, current_folder 
    render action: "new", locals: { current_folder: current_folder, destination_folders: destination_folders }
  end
  
  def create
    current_user.tells current_folder, to: :move_to, params: { destination: destination_folder }
    redirect_to folder_path(destination_folder)
  end

  protected 
  def current_folder
    @current_folder ||= Folder.accessible_by(current_ability).find params[:id]
  end 

  def destination_folders 
    @destination_folders ||= Folder.accessible_by(current_ability).excluding(current_folder).in_order
  end

  def destination_folder 
    @destination_folder ||= Folder.accessible_by(current_ability).find folder_params[:destination_folder_id]
  end

  def folder_params
    params.require(:folder).permit(:destination_folder_id)
  end
end

# a mythical controller that represents moving a folder from one location to another

Note: I have a base controller - UiController that defines current_user as well as exception handlers for CanCanCan::AccessDenied and similar common error states. It also defines current_folder as well, but I'm including it in the code snippet above for clarity.

If a user wants to move a folder to a different location, then call GET /folders/movements/new to display the form. This checks their permissions (using CanCanCan) and then displays the form, passing in the current folder and a list of permitted destination folders. The user selects their destination and hits the submit button - which calls POST /folder/movements, invoking the create action.

Within create the current user tells the current folder to move_to the destination folder. The definition of tells is in my User::Actions module which looks like this (slightly simplified to remove some C8O-specific stuff):

def tells(model, to: nil, authorised_by: nil, permission: nil, activity_type: nil, params: {})
  command = to.to_sym
  authorised_by ||= model
  permission ||= command
  activity_type ||= :"#{model.model_name.singular}_#{command}"

  params.merge!(actioned_by: self) if model.has_actioned_by_parameter_on? command

  authorised_by.authorise! self, permission
  model.send(command, params).tap do |result|
    model.record_update_by self, activity_type: activity_type
    yield result if block_given?
  end
end

The tells method has a number of optional parameters, so I can override various aspects of its behaviour. But the key ones are model, to and params.

model is the object of the sentence - in our example, it is the folder: "Alice tells the folder to move to a new location". to is the action to perform - I call it to so that the calling code reads nicely, but I rename it to command inside the actual method. And params are the parameters that get passed into the action. I originally tried to use Ruby's keyword arguments - **params - for this, but I struggled to get Rails' strong parameters working with it. I will revisit that at some point, as then the controller code would read current_user.tells current_folder, to: :move_to, destination_folder which is much cleaner.

The tells method checks that the user has permission to do this by telling the model to authorise the action - note, again, "tell don't ask" - the model may use CanCanCan, or it may use some other method for figuring out if this user is allowed to do this. Optional parameters allow me to override how this authorisation takes place, by specifying a different object authorised_by and a different action permission if needed (again, because I'm dealing with a lot of legacy data).

It then sends the command to the model using Ruby's send method to dynamically invoke a method, passing in the params. It also adds an extra parameter - actioned_by: self - if the destination method has an actioned_by parameter - that way, the model knows who is calling it - in case it needs to invoke further actions that need to be logged. The way it figures out if it needs to pass in the actioned_by parameter uses more of Ruby's in-built introspection capabilities. Every model includes an Actionable module, which defines a method has_actioned_by_parameter_on?. This inspects the list of methods on the object and checks if it has an actioned_by keyword parameter.

def has_actioned_by_parameter_on?(message)
  self.method(message).parameters.include? [:key, :actioned_by]
end

After telling the model to call that method, it then tells the model to record the activity, using record_update_by, passing in an activity_type that is either passed in as a parameter, or dynamically generated. In this case, the default activity type would be folder_move_to.

record_update_by is defined in the Actionable module that all models have included, and it writes to the activities table (in a background job).

Finally, tells yields a block, if one is given - which is a stylistic preference of mine, and then returns the result of the method call.

Because ActiveRecord is a CRUD framework, I've added in convenience methods to User::Actions that shortcut creating, updating and deleting records:

current_user.adds :document, to: folder, params: { name: "somefile.pdf" }
current_user.updates folder, with: { name: "New folder name" }
current_user.deletes folder

Each of these, like tells, checks the permissions and then, after completion, records the activity in the log. So I can be sure that everything is authorised and audited. And the style in which the calling code is written is both readable, explaining what's going on, and fits with one of the core tenets of object-oriented programming.

But I've never seen anyone else come up with a similar framework for their work.

Which is why I ask; is this genius or crazy?

Rahoul Baruah

Rahoul Baruah

Rubyist since 1.8.6. Freelancer since 2007, dedicated to building incredible, low-cost, bespoke software for tiny businesses. Also CTO at Collabor8Online.
Leeds, England