We have often felt the pain where our models need to serve too many masters. E.g. we are adding a lot of logic and callbacks for a particular form screen, but then the model becomes a pain in tests, where all those callbacks just get in the way. Or we have different forms for the same model but they need to behave very differently (e.g. admin user form vs. public sign up form).
There are many approaches that promise help. They have many names: DCI, presenters, exhibits, form models, view models, etc.
Unfortunately most of these approaches are hyped using contrived textbook examples and fall over when facing real world requirements. A few lines into the code you are left wondering:
Maybe a good insight into a real-world approach is this presentation by New Bamboo. Interesting parts with screens and code start a 5:45. While the presentation doesn't solve all problems listed above, I found it to be an example that is sufficiently rooted in reality that we should talk about it. The code base it talks about is an open source project for the UK government. Here are links to the files
InvoiceReceivedpresenter that acts like a model but does complex stuff on
InvoicesControllerthat finds an
Invoiceand wraps it into
InvoiceReceivedbefore showing it to the view or assigning user input.
Never mind the name choice of "Presenter", what do you think about this? Please add your feedback below:
Invoicemodel is very short and all the complicated view logic sits in
InvoiceReceivedpresenter, without making use of the model harder for other code (where such callbacks and validations would only be a pain)
InvoiceReceivedis always an
Invoiceplus then some. This makes it clear how to link to and find presenters (by the ID of their root model) or how to express a list of presenters for an index (as a collection of their root models).
Invoice::DefaultValidationsmodule (or similar) with little effort.
InvoiceReceived#savemethod feels like it could go into a reusable method which knows which validations to run and how creation works (mimicing AR a bit); it currently feels this needs to be copied and pasted into other presenters.
delegatecalls could have helped sticking closer to DRY (e.g.
InvoiceReceived.newcould accept an attributes hash to mimic AR a bit more (which would be generally good IMHO); might even help with complex/nested forms.
See the attached files for code examples from our discussion.
(Henning) Bad: The presenter accessors don't do any typecasting, so everything is a string all the time. This could have been done more elegantly.
(Arne) Bad: No automagic type casting on attributes – this can't be fun and there surely is a way to get it.
(Henning) Bad: They are copying a lot of field names and validations from the model to the presenter. This could have been done more elegantly.
(Henning) Bad: This probably wouldn't work well with more complicated nested forms. Nested forms are probably good to get rid of from an usability perspective, but they are so addictively useful for boring CRUD requirements.
(Arne) Bad: Duplicate Validations are bound to be problematic. This could have been extracted into aInvoice::DefaultValidations module (or similar) with little effort.
I agree. Our solution here isn't great, and is very copy & paste. The problem with a module approach is that in the models we try and use strict validations were as in the presenters we don't. Also, I think it makes some things less explicit. I'm not 100% convinced on any of the solutions.
(Arne) Bad: The homebrew InvoiceReceived#save method feels like it could go into a reusable method which knows which validations to run and how creation works (mimicing AR a bit); it currently feels this needs to be copied and pasted into other presenters.
(DS) Why didn't they use Virtus?
Because it wasn't released when we started the project! If we'd started it today, I think we would probably start with Virtus.
(Arne) Neutral: InvoiceReceived.new could accept an attributes hash to mimic AR a bit more (which would be generally good IMHO); might even help with complex/nested forms.
We did consider this. In the end we decided it was good to have only one entry point where we sanitised for mass-assignment.