Self Save Part 4
Video transcript & code
Today we come to the fourth and final episode in our miniseries about self-saving domain model objects. It's been a long and arduous trip. We've gone over numerous examples of problems associated with objects saving themselves.
It all started as an answer to a question I received from many viewers of episode #331. They wanted to know why, in this code, did I save the object separately from informing it of domain events?
Starting from there, we went on a tour of scenarios, showing some of the non-obvious downfalls of self-saving objects. Before we go on to talk about what lessons we might take from these last few episodes, let's quickly review the list of problem scenarios we've encountered.
We've noted that if a domain model saves itself, using facilities provided by activerecord, then will never be able to test that object in full isolation.
We've seen that self-saves can drastically slow down test suites with needless but unavoidable writes to the database.
We've seen that if business domain activities are tied to saves, adding a new pre-save validation can suddenly break dozens of tests at once. Even when the tests don't even make use of the fields being validated.
We saw that just as with tests, batch jobs can be badly slowed down by objects which implicitly save themselves repeatedly, instead of allowing client code to save them just once at the end of a series of actions.
We've observed how adding after-save hooks to objects which implicitly save themselves can lead to all kinds of havok, both in test and in production code.
We've seen how a self-saving method might work just fine on its own—but then cause errors down the road when it is run in the context of a database transaction.
Conversely, we saw how self-saving methods might develop hidden dependencies on being run in the context of an external transaction. When the transaction is removed, they cease to work correctly.
Finally, we saw how implicit saves hidden inside business logic can lead to flawed code that works "by accident", and which fails when the self-saves are removed.
We've born witness to all of these scenarios. And yet, believe it or not, this is only a selection from the list of problems that I came across in my research.
I felt that it was important that we get a good survey of the different kinds of issues that can crop up. If I had only pointed out one or two issues, it might have been easy to say that those were just rare cases of "doing it wrong". Hopefully by now I've succeeded in making a case that there are so many non-obvious things that can go wrong, it's just not worth it to have an object save itself in the course of executing business logic.
In United States tort law, there is a legal concept known as "attractive nuisance doctrine". If I erect a trampoline in my back yard, but fail to protect it with a fence, I can be held liable for any injuries when neighborhood kids wander onto my yard and play on it unsuperivised. The trampoline is what's called an attractive nuisance: something which, left unprotected, will predictably lead to trespassing and accidents.
The ease with which ActiveRecord models can save themselves in the midst of processing business rules functions as an attractive nuisance. Executing a business action and then immediately saving is convenient, and seems to make perfect sense. But then the unintended consequences pile on one by one.
And the worst of it is that many of these consequences are far removed, in time or in code distance, from the source of the problem. These are the "hard problems" of Rails development: why is this field nil? Why did that deferred job fail? Why does this code work in a controller action but not in a batch job?
As usual when a technique seems to go wrong repeatedly in different and surprising ways, there is a deeper design lesson here. A domain model's business responsibilities, and its ability to persist itself to a database, are fundamentally different and orthogonal roles. The one should not be tied to the other.
It may be convenient to collocate these responsibilities in the same object. But business actions should not be making decisions about persistence; and persistance actions should not be triggering business rules.
My recommendation is that you shy away from having objects save themselves. An object can still be saved in a controller action, or as a side effect of some other object being saved. But business logic should stay free of saves—and the same goes for record creation and deletion, as well.
Following this advice can sometimes lead to some head-scratching coding conundrums, especially when servicing a user requests requires changes in a bunch of different domain models. We'll look at strategies for systematically tackling those cases in a future episode, when we talk about Aggregate Objects.
But I think this is enough food for thought for today. Happy hacking!