When working on Refactoring, Ruby Edition, I realised that Separate Query From Modifier was one of my favourite refactorings. I'd been doing it for a while without realising that it had a formal name. I probably hadn't realised the full benefits of it either. From Refactoring, Ruby Edition: "When you have a function that gives you a value and has no observable side effects, you have a very valuable thing. You can call this function as often as you like. You can move the call to other places in the method. In short, you have a lot less to worry about." If you do not separate the querying code from the modifying code, code becomes difficult to understand and re-use. When I'm trying to track down a bug, I'm looking for two things: 1. The code that triggers the offending code (the query), and 2. the offending code itself (the modifier). If I have to search through a method that makes a query, then does some modification, then does another query, and some more modification, then my head starts to hurt. Which query returns the result that triggers the bug? And which modification is the bad modification? If we can separate the querying code from the modifying code, we can often achieve a better abstraction of our business rules and promote re-use. As a project evolves, we often have to introduce new trigger points for state changes. And in an agile environment, I often see the story cards evolve like this:
Story 1: Under condition 'X', 'A' should change such that...
Story 2: Under condition 'X', warn the user before making the change to 'A'
As Agile developers, we like this kind of story breakdown. After completing story 1, we can demonstrate to the user that we understand condition 'X' and the changes that should be made to 'A'. If we get it wrong, then we can fix it. Story 2 is the icing on the cake. It provides some nicer usability around the feature. It might also be a lower priority, and we might be able to release the code without Story 2 and gain some real business value before we polish it later. So the separation of the stories could be important. But if we mix query and modifier, it can become very difficult to introduce the warning, or introduce new trigger points for the desired state change.
But I've realised that Separate Query From Modifier is not always as easy as extracting conditional logic to one method, and having the modifying logic in another method. In my last post I said that in Rails, it can sometimes be difficult to re-wire multiple ActiveRecord objects of different type together according to some set of business rules without using the database as a storage mechanism, and without the validations getting in your way. We've often ended up with complex service methods that perform a query, do some modification (saving to the database), perform another query, do some more modification, and so on. You might end up with 5 or 6 queries that have to be performed in sequence. Later queries might depend on modifications that have been performed as a result of earlier queries. And the code is ugly, and difficult to re-use.
One way that we've solved this problem is to create a results object that represents the new relationships to be created. It's just a plain old Ruby object with some attributes. Let's say we're trying to build a new 'A' object, with relationships to B and C. Let's say A has_many B, and A has_one C. I'd create a results object called NewA, with an array attribute for the Bs and an attribute for the C. As I go through my algorithm, I can add my Bs and my C to the results object - without actually changing any underlying associations. (I now have a query without the modifier). Toward the end of the algorithm, I can present my results object to the user for confirmation, and if they confirm the change, then I can grab my results object and make the actual associations in the database (the modifier). I might even find that I can move some behaviour to this results object, and it will cease being a dumb data object. But even if I don't get to move any behaviour, the separation of query and modifier is worth the effort.