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?