Decouple Your Models with Form Objects
This is a modal window.
Das Video konnte nicht geladen werden, da entweder ein Server- oder Netzwerkfehler auftrat oder das Format nicht unterstützt wird.
Formale Metadaten
Titel |
| |
Serientitel | ||
Teil | 71 | |
Anzahl der Teile | 86 | |
Autor | ||
Lizenz | CC-Namensnennung - Weitergabe unter gleichen Bedingungen 3.0 Unported: Sie dürfen das Werk bzw. den Inhalt zu jedem legalen und nicht-kommerziellen Zweck nutzen, verändern und in unveränderter oder veränderter Form vervielfältigen, verbreiten und öffentlich zugänglich machen, sofern Sie den Namen des Autors/Rechteinhabers in der von ihm festgelegten Weise nennen und das Werk bzw. diesen Inhalt auch in veränderter Form nur unter den Bedingungen dieser Lizenz weitergeben. | |
Identifikatoren | 10.5446/31239 (DOI) | |
Herausgeber | ||
Erscheinungsjahr | ||
Sprache |
Inhaltliche Metadaten
Fachgebiet | ||
Genre | ||
Abstract |
|
RailsConf 201771 / 86
1
8
10
12
18
19
23
30
35
42
49
52
53
55
56
59
65
74
77
79
82
83
84
00:00
Offene MengeVerschiebungsoperatorKartesische KoordinatenSondierungSoftwareProjektive EbeneClientMathematikKlasse <Mathematik>XMLJSONComputeranimation
01:09
ValiditätCASE <Informatik>AggregatzustandKonfiguration <Informatik>Mathematische ModellierungDatenbankCoxeter-GruppeUmwandlungsenthalpieHeegaard-ZerlegungGrenzschichtablösungMathematische LogikBitBefehl <Informatik>DefaultProzess <Informatik>Kontextbezogenes SystemMultiplikationsoperatorDatenmodellAttributierte GrammatikMereologieSichtenkonzeptMathematisches ModellMinkowski-MetrikDatenfeldFächer <Mathematik>Quick-SortVarianzInverseHackerSampler <Musikinstrument>Einfache GenauigkeitTopologieMultiplikationZustandsmaschineInformationAdressraumXML
03:32
Konfiguration <Informatik>Mathematisches ModellMathematische ModellierungObjekt <Kategorie>Äußere Algebra eines ModulsEinfügungsdämpfungGleitendes MittelSichtenkonzeptComputeranimation
04:14
Wort <Informatik>Objekt <Kategorie>DatensatzMathematisches ModellAttributierte GrammatikReelle ZahlAbstandMathematische ModellierungLeistung <Physik>DatenbankStandardabweichungQuick-SortComputeranimation
04:52
ZahlenbereichVorzeichen <Mathematik>BestimmtheitsmaßDienst <Informatik>Kartesische KoordinatenClient
05:13
Mathematische ModellierungAttributierte GrammatikMathematisches ModellPay-TVMatchingObjekt <Kategorie>Äußere Algebra eines ModulsSchnittmengeMultiplikationsoperatorQuick-SortDatenfeldFormale SpracheGradientenverfahrenValiditätGüte der AnpassungSchlussregelZeitzoneStapeldateiAdressraumXML
06:38
RefactoringStandardabweichungTeilbarkeitObjekt <Kategorie>FehlermeldungPunktMathematisches ModellDatensatzVererbungshierarchieZahlenbereichKontrolltheorieDatensichtgerätParametersystemMereologieAttributierte GrammatikRegulärer Ausdruck <Textverarbeitung>CASE <Informatik>SichtenkonzeptSprachsyntheseMathematische LogikValiditätDifferenteDatenfeldMathematikEin-AusgabeKlasse <Mathematik>Message-PassingDatenbankTransaktionVariableCoxeter-GruppeDämpfungGruppenoperationTranslation <Mathematik>Interaktives FernsehenAutomatische IndexierungWeg <Topologie>Euler-WinkelFlächeninhaltp-BlockAggregatzustandMathematische ModellierungXMLComputeranimation
10:39
CodeDienst <Informatik>Zusammenhängender GraphBitApp <Programm>Objekt <Kategorie>Computeranimation
11:20
ValiditätAttributierte GrammatikZahlenbereichMathematische ModellierungFehlermeldungDatenfeldSichtenkonzeptObjekt <Kategorie>Mathematisches ModellDifferenteParametersystemMessage-PassingKategorie <Mathematik>KontrolltheorieMathematische LogikDatenbankRefactoringCASE <Informatik>Fahne <Mathematik>Bildgebendes VerfahrenWald <Graphentheorie>MathematikUniformer RaumKreisbewegungSystemaufrufForcingTypentheorieDatenmissbrauchFunktionalWeg <Topologie>p-BlockDigitaltechnikXMLComputeranimation
14:09
Mathematisches ModellKontrolltheorieTopologiePASS <Programm>Objekt <Kategorie>Array <Informatik>XMLComputeranimation
14:57
AdressraumBimodulKategorie <Mathematik>VererbungshierarchieLoopCodeDatenbankObjekt <Kategorie>Fahne <Mathematik>Rechter WinkelGüte der AnpassungHash-AlgorithmusGruppenoperationParametersystemMathematisches ModellBitVariableKonfiguration <Informatik>Mathematische LogikMathematische ModellierungRenderingDatenfeldAttributierte GrammatikMultiplikationsoperatorDefaultMessage-PassingAutomatische DifferentiationKontrolltheorieZeitzoneValiditätIterationMereologieKlasse <Mathematik>DatensatzEinsDatenmissbrauchFlächentheorieBlasePartielle DifferentiationLeistungsbewertungFigurierte ZahlPaarvergleichFaltung <Mathematik>BitrateSkriptspracheFlächeninhaltAppletSystemaufrufRechenwerkComputeranimationXML
22:16
DatenmodellValiditätMathematische ModellierungClientMathematisches ModellMusterspracheHash-AlgorithmusSoftwaretestSystemaufrufApp <Programm>Ordnung <Mathematik>Objekt <Kategorie>Ein-AusgabeKontrolltheorieSichtenkonzeptDatensatzQuick-SortIntegralRechter WinkelGruppenoperationBitFehlermeldungBitrateSchaltwerkMathematische LogikGeradeComputeranimation
24:54
Tableau <Logik>PunktAutomatische HandlungsplanungValiditätSystemaufrufSoftwareentwicklerVersionsverwaltungMathematisches ModellKartesische KoordinatenVideokonferenzVorlesung/Konferenz
26:28
JSONXML
Transkript: Englisch(automatisch erzeugt)
00:14
So, I work at an agency called Industriel. We're based out of Ottawa. I might be the only Ottawa dev that's not
00:23
working for Shopify at RailsConf, I don't know. But we're an agency and we work closely with clients. And the software is consistently evolving as we're working on it. And it's kind of just a nice way of saying that clients change their mind a lot. Which is totally fine, but the challenge is finding ways of designing software that's
00:43
open and flexible to change. So, I recently finished a project that was a big survey. It was a lot of forms. So, the whole application was a form. And before I started, I wanted to come up with a way that would be adaptable. Because we knew the survey wasn't really set in stone.
01:00
There'd be new questions to add, old questions to remove, and the path that the user would go down from one form to the next was shifting. So, this is the challenge. If your data model matches your form, this is an easy problem. Rails has excellent defaults for creating a form that matches your data model.
01:20
If you need to nest models, it means using accepts nested attributes for. And it's one of those things that's a little bit magical and I don't quite know what's happening there. But once your forms start to get a bit complicated, you have to resort to hack if statements in your models for validations. And these can get a bit unruly. It's difficult to understand the context
01:41
of what's happening. And changing it becomes more difficult. So, I've been curious about finding good ways to solve this problem for a long time. And there are countless ways of doing it. A lot sort of depends on your specific use case and requirements. For example, you can store fields in a session. That's a very good option. Say if you have a wizard where a user
02:03
fills in their information, their name, their email, their address, and then you store all that data in the session. And at each step, and the process is done and you save everything to the database at the very end. There's a gem called Wicked, which is really talented. It's splitting up single models into multiple steps. And then you add conditional validation to your model
02:21
based on which step you're on. There's a state machine, which I'm not a huge fan of. They kind of start off simple and they get unruly pretty quick. And for a form, it seems kind of like throwing a flame thrower at a candle, to light a candle or something.
02:41
Another option I've tried is to nest your models. There was a really good talk a few years ago at RailsConf by Andy Millet, and the basic idea is you have a big model, you split it up into nested chunks, and you validate each chunk separately. It's cool stuff. And all these options are great, but they're not really for me. And the reason is that a lot of these options struggle
03:00
because the models are trying to bend to presentation details. They're trying to match the form to the database to make validating the form easier. But to me, our models shouldn't care about presentation details. And I kind of totally get why this happens. Forms are kind of in this weird space. They're part view, they're part model,
03:21
they're both those things at the same time. And we all know we're taught not to put business logic in our view, but what's never said is not to put view logic into our models. So let's not do it. This is Ruby, after all, and we can do whatever we want. So if those previous options I mentioned,
03:43
I don't really like them, what is a good option? So I really like form objects. It's a good alternative. They're model agnostic in that not all your data needs to live on one big model. You can have small models that are tightly focused, and just grab what you need for the form.
04:02
They're kind of these custom layers between your view and your model. And I'm gonna take you a few ways of writing them. I'm gonna roll your own, and I'm gonna use a gem called reform. And first of all, let's just define it, like what is it? So put simply, a form object's just an object
04:21
that we pass into form for. So this is sort of the standard way of creating a form. We're just passing in a person, active record model into form for here. This is not a form object, but the idea is the same. It doesn't have to be an active record model. It can be anything you want. And this is the real power behind form objects.
04:40
Instead of being limited to active record models, we can compose objects from any attribute in our database. And as you'll see, this opens up the doors for some really flexible designs. So let's look at an example to make things clear. We're gonna be building a service where dog walking companies can sign up
05:01
and manage their clients. So a user signs in, and this is the onboarding wizard before they can use the application. It consists of three forms. The first thing we ask for is the name and phone number of the company. On the second form, we ask them to add in some addresses, and they can add or remove multiple addresses. And finally, there's a settings panel
05:21
where we ask for their name, the size of their company, their subscription type, the time zone, their language preference, and that's it. So just those three forms, and it seems deceptively simple. But there's some challenges here. So in the first form, the challenge is nested data. The phone model is a child of company,
05:42
and is there a better alternative than accepts nested attributes for? In the second form, our data matches our model pretty closely. Exactly, in this form. I'm sort of wondering, is using a form object in this example a good alternative or not?
06:02
What are the advantages of doing that? And the challenge of the third form is that data is scattered all over the place. As you'll notice, we're saving to company again, and all these fields are required, and we're gonna save each step as we go along each form. And this can make validation difficult if we had to validate the entire model when it's saved.
06:21
And we can imagine, outside of this example, there might be many more attributes on these models that would have their own rules for validation, perhaps their own forms. This is just a simple example for the purpose of this talk. But you know if you've built forms, things get complicated quick. Okay, so let's build the first one.
06:41
I'm gonna show you how to write this using a form object, just with tools that Rails gives us. And then I'll refactor that with reform, and then we'll compare afterwards. So this is your standard Rails controller. Nothing too exciting here. The only difference is the instance variable
07:02
for our view is company form.new. For the create action, we're passing in some company parameters, and they're strong parameters, and the attributes are name and number. And even though number is nested, we don't have to remember what the syntax is for strong parameters.
07:21
It doesn't matter in this case. And this is our form view. I'm using simple form here, but form four works great too. And you notice we're just passing our company form. We have our name for our company and our number for phone. And notice that these form inputs are flat.
07:41
There's no nesting happening here at all. No need for a form fields block, even though the phones are nested under company. So let's make our form object. We just make a new class. We include active model model, which will give us validation, translations, and allow us to create an object that's very close to an active record model.
08:00
And we do that by defining some attributes, name and number, and then we define those attributes where they're gonna go to once they're saved. So in this case, a new company and a nested company phone. Next we add in validation. This is your standard presence validation. You can get fancy here with some Regex on the phone number and do all kinds of stuff,
08:20
but we're just gonna keep it simple for this presentation. And the next, this points to a method. And the purpose of this validation method is to display errors, validation errors. So Rails uses accept nested attributes for to display nested error messages. So if we didn't define this method,
08:40
any nested models, like our phone model here, would fail validation, but the error messages wouldn't show up because they're nested under company. And they're not gonna be bubbled up to the parent. So we write this method here, display errors, and that's all it does. It just bubbles up error messages from child relationships up to the parent.
09:02
And speaking of which, we need to create that. So this method will now act like a company model. Company is gonna be our parent in this case. And it's gonna point to the company controller in the same way that the company model would. Next we have to define our own save method.
09:20
So let's do that. So how this works is when we call save, it's gonna run through the validations. If it's not valid, it's gonna bubble up our error messages. If it is, we create a transaction. And then that calls save on everything. And these are bang methods, so if anything here fails, it's gonna be rolled back
09:41
and nothing's gonna actually be saved to the database. And there's no way for the controller to infer the ID of the company. And we're gonna need that when we redirect to the next form. So we just create that. We just need to create the company ID method and we're just gonna create that in our form object just with a delegate.
10:01
That's our form object. So not too difficult to write, stays really close to the Rails way. Our controllers don't change much and it's a nice way to do it. The tricky thing about rolling your own is you're on your own. You have to write every little thing from scratch, from saving to bubbling up validations.
10:22
And the more complicated your form gets, perhaps the more complicated logic you need to figure out on these kind of base layer things that you might need to do. So there's a gem out there called Reform. And it's part of the Trailblazer ecosystem maybe. So Trailblazer is a talk on its own.
10:43
And I'm not gonna go too deep down that rabbit hole, but essentially it's a way of extending MVC. So the basic premise is you organize your code not by model, view, controller, but by concept. And inside each concept, or a component maybe you could call it, are service objects called operations.
11:01
And Reform is one kind of operation, the purpose of which is to build forms. And the nice thing about Trailblazer is you can use just bits and pieces of it. You don't need to use the whole thing. And in the last app I built, I just used Reform, and it worked out great.
11:20
So let's refactor this form object that we just built to use Reform. So assuming we've installed the gem, we need to inherit from Reform form. Reform doesn't know anything about Rails, so we'll remove active model model. Next we have a very similar idea to our accessor methods here.
11:42
And they're called properties. And they perform the exact same function, defining and writing to our attributes. We previously had to define the model that this object is gonna be talking to. And one difference is in the controller here, Reform needs an argument, and so we're gonna do the exact same thing by passing in our company.
12:02
So it is passing company.new into our form object. And we can get rid of this. And we don't need this delegate method anymore. So I'll talk more about validation in general, but the nice thing is that all this custom logic we wrote to handle error messages
12:22
bubbling up from nested models is irrelevant. It's taken care of for us. So let's get rid of that. And now because we're passing in a new company from the controller, this method is already defined for us. It can infer what the new company is. And it's now on the form object. Or it's now on the controller now instead of on the form object.
12:41
So what we're left with is initializing the nested phone method. And we haven't passed it in through the controller, so we need to define it here on the form itself. Because it's a nested object under company, Reform has a specific way to define that type of relationship. It's called a collection.
13:00
It's a block. And you just call it by the same name as the thing you're nesting. So phones in this case. And then we define the properties inside the collection. So number in this case. And the one thing that has to change is our form. So we can no longer keep this flat view that's kind of nice. We have to introduce fields for here. And fields for just matches to collections inside Reform.
13:23
And for saving, we don't need to write our own logic to save it to the database. We'll do something mildly different on the controller to account for that. But for now, let's get rid of those strong parameters. Strong parameters are no longer a thing that we need to worry about.
13:40
We set these attributes when we define properties on the form object. And Reform will ignore any undefined parameters. So we can just safely remove those. And we'll just pass in a company here. And the main difference here is how we save it. In the other form object, we made sure to validate before saving. And instead, we'll do this on the controller here.
14:01
So we just call companyForm.validate, we pass in the parameters that the form gives us, and then we call save. But something is wrong. Should look like this, but it looks like this. Maybe you've had that experience before. So to fix that, we can do something like this.
14:21
We just need to instantiate that nested model on the controller. And this will work, but Reform has its own method for just this occasion. And it's called pre-populate. And we'll define that in the form object. All we need to do is add a pre-populator method to the collection.
14:42
We're calling it buildPhone, but you can call it whatever you want, and it does pretty much what you'd expect. We grab our collection, and collections are in arrays, so we can't call .build or .new on it. And you just treat it like an array, and you append a new phone object, phone on it. Now it's displaying correctly.
15:02
I think it's looking pretty good. One thing we have left to do is make sure that our primary flag gets set to true when we save this form. Let's remove this leftover from the previous example, and we'll deal with that. It's really easy. All we need to do is add the property primary, set a default. It's done.
15:22
So one issue, though, is if you try to run this, if you run your tests, phones aren't saving. And the reason is that reform makes no assumptions about nested data. It understands the parent object company. We've passed that into the form object, but collections are different. So even though we've defined it, reform won't assume to know which model
15:41
it's supposed to persist to. So we need to tell it. And we do that with something called a populator. So now I know what you might be thinking. There's pre-populators, populators, what's going on here? So an easy way to think about it is pre-populators are your new and added actions.
16:00
They're called manually with pre-populate. They prepare the form for rendering. They're all about displaying it. And they can fill in some default fields into the form itself if you want. Populator's about creating and updating. They're called every time you validate. And they prepare the form for validating, and they make sure that everything goes back
16:21
into the right model where it's supposed to go. So at its most basic, you can add a setting to the collection called populate if empty. And you just pass in the class that you want to map it to. And that'll make sure that when you validate, it's gonna persist to the phone class and try to validate against that. So that's our form object.
16:41
We just need some validation in there, and we're good to go. And that's it. So that's our first form. Two ways to write it. And the first way, we rolled our own. The second way, we used reform. And here's a side-by-side comparison. So on the left is rolling our own reform on the right.
17:01
And it's pretty obvious that there's a lot less code to write for the reform option. For the controller, I would say it's a bit of a wash. The roller, your own, on the left is a bit cleaner and simpler because we just call save on it. But we don't have to use strong parameters anymore. It's kind of a nice added side benefit.
17:22
So let's go on to the next one. I'm gonna go quickly through the next ones. So this form is adding and removing addresses, and it's happening on one model, the address model. This is our controller. We're using the edit and update actions,
17:41
and we're making a new address form. And we're passing in our previously created company right into that form. And we're calling pre-populate. In our form, we have a couple things going on. We have fields for addresses here, and it's calling a partial called address form.
18:00
And inside that partial, it looks like this. And this is just, it has all of our fields for address. And it has this hidden field called destroy. And that's just gonna mark it as one or zero. Like if you say destroy this thing, it's gonna mark it as one, otherwise it's gonna be zero. And then we just have some custom helper here
18:22
that with a little JavaScript is gonna add a partial, and you can just add more addresses when you click a button, remove one when you click another button. So in our form object, we inherit from reform form. We add in our collection for addresses.
18:40
Destroy is flagged as a virtual attribute, so it's not actually anything that's going to the database. For a pre-populator, we're doing very much the same thing we did before, just making sure we have one new address to display on the form. And our populator, we're gonna have to write some custom logic to handle the adding and removal of addresses. So previously we wrote populate if empty,
19:02
and pass in our class. And now we're gonna write our own custom thing. So there's a couple methods in here that are part of reform's DSL. Fragment and collection. So collection is gonna iterate over all your addresses.
19:20
So if you called company.addresses, you might get five addresses back. That's the collection. The fragment is each individual address with an ID. And the populator will loop through every record, every fragment inside the collection. So we see if we have an address. We define it as a local variable.
19:41
We try to find it on the collection to see if there's an ID of a fragment on there. And if we find a fragment, everything is good. We return that. If it's empty, we make one. And if we see that on the fragment destroy set to one, we delete that. We delete that fragment from the collection.
20:02
And then the skip method will just skip to the next fragment in the loop. And the last thing we need here is our validation. And we'll just pop that in there. And we're done. And we can now add or remove multiple addresses here. And the last form in this example,
20:21
this one's kind of tricky because data's all over the place. So we have some stuff from company, some stuff from user and account. And we previously saved to company, so we don't want to necessarily revalidate company again. We just wanna validate it every time we put something in the database.
20:42
And it's really easy to solve this. So this is our form. And notice that there's no fields for it anywhere in here. It's flat. There's another way to do it. We're just listing attributes. We're not caring about the data. And we're gonna define that data on the controller. So there's a company, there's a user, an account. And then all we're gonna do with this form
21:01
is we're gonna pass those three models as a hash into our form object, just like that. And in our form object, we include this module called composition that comes with reform. I'll show you what it looks like. So we define the company as the parent model that we're gonna save to.
21:21
And that's really easy. We just add in our properties, and then we just tell it which properties belong where. And we do that with this on attribute. So time zone is on company. First name is on user. You get the idea.
21:40
Anywhere we might have a name clash, like each record's ID, we just give it a new property name, and we tell it where it's from. And because we're explicitly passing in objects into the form object with the hash and using composition, reform can just figure out where everything is saving to.
22:02
So there's no need to define populators or pre-populators on this form. And the last thing we do is just add in our validations, and it's all done. So what are the advantages of doing this?
22:23
So form objects are model agnostic. You no longer need to even consider the view when setting up your data model. And there's kind of an ancillary benefit to this, which is small, tightly focused models, not these giant behemoth models.
22:42
And not that I'm recommending it, but you could forgo having any validation on your models whatsoever. And instead rely on the fact that the right data is getting to the right place based on the inputs that are coming in. And at the very least, this means no conditional validation in your models.
23:02
We're validating the form, not the model itself, sort of a different way of thinking about it. And this follows REST. We essentially have one controller per form, and it's just following the REST action. It's a very tightly focused and easy to understand controller. There's nothing too exciting about it.
23:22
These are easy to test. So to test these forms, I would typically have a capybara integration test running through the whole wizard. And then testing the form objects themselves is really nice. With reform especially, you just pass in a hash into the form and call validate on it.
23:40
And you can just make sure you're getting the right error messages that way. You don't need to set up a whole active record model to pass into the form. You can just set up the data that you're passing into the form. And so those tests are nice to work with. They're fast, and they're easy to write. So this is also easy to extend and modify.
24:03
If our client comes back and wants to change the order of our wizard, or wants to change which attributes appear on which forms, we can do so without much effort. And the last thing, you can use them when you need to.
24:23
There's nothing wrong with using the Rails defaults, but it makes sense to do so. But as soon as things sort of get a bit more complicated, this is a really good tool to have in your back pocket. So give it a try on your next form.
24:41
See what it's like. You might really like it. It's a nice thing. You can just incorporate it into legacy apps. It's a nice little pattern. So thank you. Yes, the question is about the reform jam. How stable is it? I think it's very stable from what I've been,
25:01
what I've been working on it. It's very actively maintained. So I don't know what the point releases are gonna be like. Right now it's in version two. And I think there's some big plans for three. I'm not sure about that. So I'm not sure. But so far it's been really, really nice to work with,
25:22
and I've just been using the latest jam, and it's been working really great. Yeah, so the question was about forgoing validation on the form, and when you decide to validate just the model or just the form. And I guess it's like a,
25:41
you kinda have to make that call. Like when is it crucial that your company has a name? Like if it doesn't have a name, and the whole application blows up, then maybe it makes sense to have it on the model. And if you're, say, in the Rails console or something, and you as a developer are like creating a company for somebody,
26:02
maybe you wanna make sure that the company at least has a name. So just kinda play it by ear and just, yeah, you kinda wanna be careful with that stuff, but in general for like the stuff that's not too, too critical, I would just say leave it off the model.
26:21
Okay, well thank you very much for coming. I'll see you in the next video. Bye.