Geek Alert: This is a technical post for software engineers - those of you uninterested in such things, feel free to skip it (and all posts like it).
We do a lot of things in Software Engineering that seem to be overcomplication for its own sake. One of those is, perhaps, the principle of separation of responsibilities and concerns, partially described by SRP: The Single Responsibility Principle [PDF]. However, I believe most engineers agree that when used with care the benefits of this type of separation, more often than not, outweigh its costs over the life of an application. Certainly Martin Fowler would agree, and he's a kind of rockstar in the agile development world - which if any camp was going to be "anti-separation" I would expect it to be them.
Unfortunately, though, having strict separation creates a scenario not unlike the one depicted in the book, Harry Potter and the Goblet of Fire, quoted as the subject of this post.
A Tablet PC application I worked on utilized a common MVC pattern. We had a View (the User Interface, made up of Forms, Dialogs, etc), a Controller (various controllers, actually, each with their own set of concerns), and a Model (in this case, a Business Object Layer (BOL), Business Logic Layer (BLL), and Data Access Layer (DAL)). Controllers manage critical UI behavior (beyond simple form interactions) and mediate between the View and the Model. The View does not directly cause action in the Model nor does the Model directly interact with the View (a possible exception being, well, exceptions, which if not handled at the controller layer will bubble up into the View).
There was an activity, a long-running activity, that happened in the Model layer. It prepared a large amount of data for export to another system by compiling it into a compressed file and, at the same time, updated the state of that data. That all happened within a single Transaction, such that if any part failed, the whole transaction was rolled back.
This all worked well, but a new requirement was introduced. The BLL (part of the Model) now had to determine as part of its processing whether the amount of data is "very large" (exceeds some configurable limit) and may result in a timeout during export. If the data is "very large" then the user is to be notified and given three options:
The first two options are simple enough to handle. Since an export is going to be executed in either case, we can perform the first part of processing (gather all the export data and update data states), then validate the size of the export data, receive user input if needed, and finally perform the export (either to web service or local filesystem). Since these scenarios involve an entirely serial workflow, we just modify our sequence diagram slightly.
But wait a minute - there was a third option, right? The one that reads, "Cancel the export altogether." No problem - if the user wants to cancel then just don't make the ExecuteExport call, right? right? Not exactly. See - there's a problem. During the preparation step it updated the data states of all the data to be exported. This all happened within a Transaction such that any failure to complete the task would result in a roll-back of those data states. In fact, this same issue arises with the first two options as well, because there is no guarantee that sending the export data is going to succeed (even SneakerNet fails occasionally...)
This creates a bit of a pickle. Easily resolved, of course, by moving the Transaction Root out of the Model and into the Controller - but should a Controller really be managing transactions? In general, it is assumed that the logical place for transaction management is the BLL and it makes sense because that is where the truly "critical" business decisions are made.
If determining which changes are persisted and which are rolled back is a business decision (hint: it is) then transaction management ought to remain firmly planted inside the Model and away from any UI Controllers. And if the Controller can't host transactions, then it certainly shouldn't be responsible for canceling the export and rolling back data states. What to do?!?!?
The solution actually turned out to be fairly simple: fire a synchronous event from the BLL, which is handled by the UI Controller, that returns the user's response back to the BLL within the event arguments. This allows us to maintain isolation of transaction management in the Model while gaining user interactivity in the midst of a long-running process.
Here's the final sequence reflecting the addition of an event and evaluating the result of the user's response.
Remember Me
a@href@title, b, blockquote@cite, em, i, strike, strong, u
The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.
E-mail