This card is ancient legacy. Please see Growing Rails Applications in Practice and ActiveType for our current take on this topic.
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:
- How would this work in forms?
- How would this work in nested forms?
- How can I have validations that attach errors to fields, so erroneous form fields are highlighted automatically?
- How can I have custom callbacks for form models with the callback syntax from ActiveRecord which we know and love?
- How do I load or link to a form model? Since it doesn't have an ID etc.
- How do I present a list of form models, e.g. for an index action? Examples usually don't show a way to load a collection of such models.
Maybe a good insight into a real-world approach is this presentation by New Bamboo Show archive.org snapshot . 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
-
InvoiceReceived
presenter that acts like a model but does complex stuff onsave
-
Invoice
model -
InvoicesController
that finds anInvoice
and wraps it intoInvoiceReceived
before 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:
Good
- (Henning, DS) Good: The
Invoice
model is very short and all the complicated view logic sits inInvoiceReceived
. - (Henning) Good: Ability to have additional validations and callbacks in the
InvoiceReceived
presenter, without making use of the model harder for other code (where such callbacks and validations would only be a pain) - (Henning) Good: Their presenters are always rooted in a model, so e.g.
InvoiceReceived
is always anInvoice
plus 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). - (Henning) Good: They are using a separate class for the presenter instead of extending objects DCI-style. This allows them to add statically-defined callbacks and validations from ActiveModel with the syntax that we know and love, which is not possible when extending an object with a module.
- (Arne) Good: Generally a good approach, I concur with Henning. :)
- (Arne) Undecided: While the model itself stays slim and validations don't get in the way of tests, validations don't get in the way of tests meaning that developers need to be aware of using integration tests. Model/controller specs alone are probably not enough (they rarely are, but especially in these cases).
Bad
- (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.
- (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.
- (DS) Their presenter may keep them from reinventing the app logics-wheel, but feels like reinventing the Rails-wheel.
- (Arne) Bad: Duplicate Validations are bound to be problematic. This could have been extracted into a
Invoice::DefaultValidations
module (or similar) with little effort. - (Arne) Bad: No automagic type casting on attributes -- this can't be fun and there surely is a way to get it.
- (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.
Comments
- (DS) Why didn't they use Virtus?
- (Arne) Neutral: Maybe a few
delegate
calls could have helped sticking closer to DRY (e.g.InvoiceReceived#loans
) - (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. - (Henning) Heretic question: We should be clear about why a form model is a better place for screen-specific code than a plain old controller. Weren't controllers originally the place for that? Then someone yelled "fat controllers are bad" and maybe everyone cargo-culted their way from there. => We think having an ActiveRecord-like workflow "validate everything, mark fields with errors, then save all or nothing" is very useful, and it's hard to emulate this in a controller.
Code examples from our discussion
See the attached files for code examples from our discussion.
Reply from the presentation author
(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.
You're right. We have recently started to mixin a concern and use it like so.
(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.
In our LoanPresenter we do define a .attribute
method which allows us to define (and delegate) a lot more cleanly.
(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.
We do actually use nested forms to good effect, but we don't use the ActiveRecord
helpers. Instead we define a *_attributes
method
5
Show archive.org snapshot
and then fields_for in the view
6
Show archive.org snapshot
.
(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.
This case is actually quite specific and so we can't have a generic solution. We do have an abstracted version in our LoanPresenter and LoanStateTransition.
(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.
Cheers, Olly