Crafting Wicked Domain Models
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 | ||
Anzahl der Teile | 110 | |
Autor | ||
Lizenz | CC-Namensnennung - keine kommerzielle Nutzung - 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/51021 (DOI) | |
Herausgeber | ||
Erscheinungsjahr | ||
Sprache |
Inhaltliche Metadaten
Fachgebiet | ||
Genre | ||
Abstract |
|
NDC Oslo 20122 / 110
1
2
3
5
7
9
11
12
15
19
20
23
24
27
28
29
31
32
33
35
36
37
38
39
41
43
46
47
51
52
56
59
60
61
62
63
65
67
70
71
74
75
77
79
80
81
83
87
91
92
93
94
95
96
97
98
100
103
106
108
110
00:00
Mathematisches ModellProblemorientierte ProgrammierspracheCOMDemo <Programm>Business ObjectKartesische KoordinatenUnternehmensarchitekturMusterspracheTermMathematische LogikMathematisches ModellDifferenteProblemorientierte ProgrammierspracheSchnitt <Mathematik>Mathematische ModellierungBitGüte der AnpassungMathematikProgramm/QuellcodeXMLUML
01:24
SoftwareentwicklerQuelle <Physik>Schreib-Lese-KopfMathematische ModellierungProblemorientierte ProgrammierspracheAggregatzustandMathematische LogikAggregatzustandMathematische ModellierungKomplex <Algebra>Element <Gruppentheorie>TabelleProgrammierungKartesische KoordinatenReelle ZahlDatenbankMultiplikationsoperatorUnternehmensarchitekturSchlussregelInformationNatürliche ZahlProdukt <Mathematik>DifferenteE-MailPlastikkartePunktUmsetzung <Informatik>Gebäude <Mathematik>FrequenzWeb-SeiteLastMatchingGamecontrollerInformationsspeicherungEINKAUF <Programm>CodeObjekt <Kategorie>CASE <Informatik>Problemorientierte ProgrammierspracheMereologieMathematisches ModellTouchscreenObjektmodellPhysikalisches SystemQuick-SortSelbstrepräsentationRechter WinkelErwartungswert
07:30
E-MailZeichenketteSoftwareentwicklerSchreib-Lese-KopfQuelle <Physik>DatentypENUMDifferenteQuick-SortMailing-ListePhysikalismusTermTypentheoriePhysikalisches SystemMereologieEinsZahlenbereichDynamisches SystemLoginStützpunkt <Mathematik>InformationGebundener ZustandWeg <Topologie>CASE <Informatik>Güte der AnpassungMultiplikationsoperatorMatchingWeb SiteKlasse <Mathematik>Ganze FunktionENUMAdressraumE-MailAssoziativgesetzWiederkehrender ZustandKartesische KoordinatenComputeranimation
11:13
Klasse <Mathematik>Quelle <Physik>E-MailDatentypRichtungPhysikalisches SystemDemoszene <Programmierung>ZeitrichtungFramework <Informatik>TabelleAssoziativgesetzMathematische ModellierungKategorie <Mathematik>Klasse <Mathematik>Computeranimation
12:41
Schreib-Lese-KopfQuelle <Physik>Dienst <Informatik>DatentypAbzählenMathematisches ModellDatenverwaltungDienst <Informatik>Klasse <Mathematik>GruppenoperationHilfesystemLastGamecontrollerBitWeb-SeiteMathematische ModellierungKartesische KoordinatenMathematisches ModellProblemorientierte ProgrammierspracheKoordinatenDifferenteMereologieComputeranimation
14:17
Mathematisches ModellProblemorientierte ProgrammierspracheSoftwareentwicklerQuelle <Physik>Schreib-Lese-KopfBusiness ObjectObjektmodellPhysikalisches SystemKartesische KoordinatenDatensatzProblemorientierte ProgrammierspracheVersionsverwaltungDatenbankTermMathematische ModellierungDienst <Informatik>Mathematisches ModellKategorie <Mathematik>CASE <Informatik>Objekt <Kategorie>Klasse <Mathematik>Computeranimation
15:38
OvalSoftwareentwicklerDokumentenserverQuelle <Physik>DatentypSchreib-Lese-KopfCodeLastGrenzschichtablösungGeradeInjektivitätVorzeichen <Mathematik>Konstruktor <Informatik>ParametersystemZahlenbereichDatenbankObjekt <Kategorie>InformationsspeicherungDienst <Informatik>Kondition <Mathematik>Computeranimation
17:04
DefaultDatentypSchreib-Lese-KopfQuelle <Physik>SoftwareentwicklerMathematisches ModellMultiplikationsoperatorVorzeichen <Mathematik>SoftwaretestGrundraumInformationCodeZahlenbereichBefehl <Informatik>Kartesische KoordinatenCASE <Informatik>EinsPhysikalisches SystemMatchingKomponententestSoftwareentwicklerProzess <Informatik>Problemorientierte ProgrammierspracheGeradeProgrammfehlerHilfesystemCoxeter-GruppeExistenzsatzRechter WinkelGüte der AnpassungComputeranimation
19:39
Quelle <Physik>Schreib-Lese-KopfSoftwareentwicklerRechenschieberOvalDefaultDatentypPhysikalisches SystemGenerizitätZeichenketteE-MailProblemorientierte ProgrammierspracheMathematische ModellierungExogene VariableDienst <Informatik>VisualisierungRechter WinkelSchreib-Lese-KopfGüte der AnpassungInformationE-MailUmsetzung <Informatik>Ordnung <Mathematik>Interface <Schaltung>MagnetkarteObjekt <Kategorie>Mailing-ListeAdditionMereologieKonstruktor <Informatik>Objektrelationale AbbildungDatenfeldPhysikalisches SystemTouchscreenLesen <Datenverarbeitung>BildschirmfensterInternetworkingDatenbankCASE <Informatik>CodeQuick-SortBefehl <Informatik>Metropolitan area networkObjektmodellFunktionalVarianzKlasse <Mathematik>AbzählenKategorie <Mathematik>TypentheorieMathematisches ModellVorzeichen <Mathematik>SkalarproduktEin-AusgabeTeilbarkeitSpiegelung <Mathematik>InformationsspeicherungEinsModifikation <Mathematik>SoftwareentwicklerEinfügungsdämpfungProgrammierungInvarianteErneuerungstheorieDemoszene <Programmierung>Nichtlinearer OperatorSoftwaretestProgrammfehlerKartesische KoordinatenZahlenbereichRandwertComputeranimationProgramm/Quellcode
28:40
Mathematisches ModellMinkowski-MetrikE-MailZeichenketteSoftwareentwicklerRechenwerkDokumentenserverDatentypDefaultPhysikalisches SystemSpezialrechnerEingebettetes SystemOvalHash-AlgorithmusFehlermeldungObjekt <Kategorie>KontrollstrukturVisualisierungCompilerDatensatzRechter WinkelRoutingInformationExogene VariableWeg <Topologie>ProgrammfehlerEinsProdukt <Mathematik>SoftwaretestKartesische KoordinatenCodeSampler <Musikinstrument>AggregatzustandZahlenbereichRechenbuchKomponententestGüte der AnpassungFlächentheorieKonstruktor <Informatik>MereologieGeradeRechenwerkE-MailPhysikalisches SystemKategorie <Mathematik>BitUniversal product codeSchnittmengeNichtlinearer OperatorTaskMultiplikationsoperatorValiditätSoftwareentwicklerTouchscreenEinfache GenauigkeitPunktTermKlasse <Mathematik>SchlüsselverwaltungRefactoringCASE <Informatik>Dienst <Informatik>GeheimnisprinzipVorzeichen <Mathematik>TypentheorieSchlussregelApp <Programm>ComputerspielProgramm/QuellcodeComputeranimation
37:11
SoftwareentwicklerDefaultKovarianzfunktionDatentypZeichenketteE-MailOvalLokales MinimumMathematisches ModellPhysikalisches SystemModallogikGegenständliche BenutzeroberflächeLASER <Mikrocomputer>Minkowski-MetrikBefehl <Informatik>Computerunterstützte ÜbersetzungZahlenbereichVorzeichen <Mathematik>Physikalisches SystemMathematische ModellierungTypentheorieValiditätBenutzeroberflächeInformationRechter WinkelBusiness Objectsinc-FunktionPunktDienst <Informatik>Lokales MinimumSchnittmengeQuick-SortKonstruktor <Informatik>CASE <Informatik>GamecontrollerKlasse <Mathematik>DatenbankENUMRichtungNichtlinearer OperatorLastObjekt <Kategorie>AppletReelle ZahlKartesische KoordinatenRechenbuchProgrammierspracheMereologieKardinalzahlAbzählenMultiplikationsoperatorGanze ZahlGewicht <Ausgleichsrechnung>ExpertensystemMessage-PassingCodeEinfache GenauigkeitObjektrelationale AbbildungComputeranimation
45:42
Minkowski-MetrikMathematisches ModellSoftwareentwicklerStatistikZeichenkettePhysikalisches SystemDatentypLASER <Mikrocomputer>MathematikModallogikE-MailGenerizitätMagnettrommelspeicherVirtuelle AdresseRankingParametersystemKonstruktor <Informatik>ZahlenbereichExogene VariableInformationTypentheorieDatenfeldMicrosoft dot netRechter WinkelMultiplikationsoperatorCASE <Informatik>CodeObjekt <Kategorie>VererbungshierarchieNichtlinearer OperatorDatensichtgerätArithmetisches MittelAbzählenFigurierte ZahlCoxeter-GruppeBrennen <Datenverarbeitung>AusnahmebehandlungRechenbuchSpannweite <Stochastik>Befehl <Informatik>BitQuaderKlasse <Mathematik>ENUMMailing-ListeVarianzSkalarproduktGüte der AnpassungReelle ZahlObjektorientierte ProgrammierspracheValiditätDatensatzGewicht <Ausgleichsrechnung>AppletEinsHalbleiterspeicherLesen <Datenverarbeitung>ZweiC sharpComputeranimation
54:13
ZeichenketteDatentypSoftwareentwicklerGammafunktionRechenwerkMinkowski-MetrikMathematisches ModellAbstraktionsebeneKovarianzfunktionPhysikalisches SystemKlasse <Mathematik>MenütechnikParametersystemKonstruktor <Informatik>VersionsverwaltungBefehl <Informatik>MultiplikationsoperatorProblemorientierte ProgrammierspracheCASE <Informatik>TypentheorieGeradeVererbungshierarchieValiditätRechenbuchRechter WinkelObjekt <Kategorie>InformationKlasse <Mathematik>SicherungskopieDifferenteInstantiierungUmwandlungsenthalpieModifikation <Mathematik>AbstraktionsebeneRefactoringDemoszene <Programmierung>Mailing-ListeEinsKategorie <Mathematik>ExistenzsatzAbzählenMathematische ModellierungDatenbankNichtlinearer OperatorGewicht <Ausgleichsrechnung>SoftwaretestKartesische KoordinatenMathematisches ModellRandwertProgrammfehlerDelisches ProblemComputeranimation
01:02:43
SoftwareentwicklerComputeranimation
Transkript: Englisch(automatisch erzeugt)
00:02
Well, good afternoon, everyone. I have been assured that these cables are very strong and I probably will not fall during the course of this presentation, but if I do, I can trust y'all to grab me before it happens, right? Well, we'll see how the talk goes and then maybe they change your mind.
00:23
Maybe you'll cut it, yes, of course. Well, that's some incentive on my side then. So this afternoon, I'll be talking a little bit about domain modeling.
00:41
Now, domain models have had a lot of different names throughout the years. Before the term domain model came about, these were also called business objects or business logic layer. There have been a lot of names ascribed to what we now call domain models. The term domain model originally came
01:00
from the Martin Fowler book, The Patterns of Enterprise Application Architecture. That's the book you're supposed to have on your desk that tells everyone you're really important. That one in like a probably big WCF book or something like that. And inside that book, it was just a very small portion of it that described this concept of a domain model
01:21
or business objects. And so looking at what the domain model is, from the Fowler book, he described a domain model as an object model of the domain that incorporates both behavior and data. So a few key things there. One, it's an object model of the domain.
01:42
So that means we're not building something for some other domain. If we're talking about e-commerce, then we're not gonna have things like a giraffe in there or an elephant. It'll probably have things like shopping carts. So things are gonna match up to names to things that the business expects. If the business saw our domain model
02:01
and looked at the names, they'd say, yeah, that represents what we're talking about. The other two things the domain model contains are data and behavior. And those are the two key parts. It's the names match what the business expects, and then it has both data and behavior to represent information and behavior.
02:24
This goes beyond just the nouns and verbs, so time out.
02:41
So we're going beyond just the nouns and verbs of I have a giraffe, giraffe is an animal, an animal has four legs, that kind of thing. We want to incorporate the data that is in our domain as well as some sort of behavior. And so a lot of times what I see people do is they'll read either the Fowler book
03:00
or the Evans book. You know I'm talking that big blue book? That's another one of those books by Eric Evans, Domain-Driven Design. That's another book you're supposed to have on your desk to make yourself look smart. So those two books describe this concept of domain models. But why would I care about these domain models?
03:21
Well, for the most part we really shouldn't. Most of the applications we build don't really have a whole lot of behavior in them. There's no real need to build rich object models to support that behavior. Most of the time I have screens to edit stuff and the user clicks save and it saves some things and then they have some screen to show some stuff and that's pretty much all we need.
03:42
So for a lot of applications there's no real need to build any kind of real business logic layer. It's just something that can talk to the database and perhaps some easy way to represent the data that's in there. So most of the time we shouldn't care about building a real domain model. Most of the applications we build out there just don't need it, they just don't require it.
04:03
But some applications do. Applications that have a lot of complexity. For example we had one that the actual persistence layer was just one table but behind that table were all the pricing rules of an enterprise application. So this billion dollar company had pricing rules
04:20
based on all their different customers, different segments, different products, different SKUs. It was persistent very simply but the actual behavior behind those were very complex. As you can imagine sales guys wanted to do pricing rules, everyone's gonna do something different. Just the nature of what they do. So a lot of times we may see that
04:41
coming around the corner. We may see that when we're having conversations with a business we can recognize that this is going to be complex so I'm gonna need to model this very carefully to make sure the things I'm building match the concepts of the business and it's not just thrown behind the controller or the page load or something like that.
05:01
But for most applications that I work with they don't start out that way. We don't know they're going to be complex when we start. They just become that way. Any application that's being used for a long period of time just will have features being added, added, added, code being added and eventually get to the point where well the thing that you wanted to build,
05:21
this nice domain model isn't there and you need to get from where you are right now to that nice end point of a business logic layer, a business object, a domain model that actually represents the data and the behavior. So what I'd like to do is walk through a real world example of a case where we ran into
05:40
an application which started out very simple. It started out as just things had data and that was it. There wasn't really a whole lot of behavior. But over time we saw lots of things get added. So I wanted to walk through this example together. So in a current state of our application this is a system that did loyalty programs or membership reward kind of programs.
06:02
Those programs that you go shop somewhere, they ask you if you're a member of their program and you say no so they give you a little card. The next time you come through you swipe your card and then you get points and if you get enough points you get some kind of reward. Maybe it's a coupon, maybe it's a gift in the mail, something like that.
06:21
So does anyone have those kind of rewards? I've got like five of those cards in my back pocket right now. So these are pretty popular because one, the customer gets something out of them. They get coupons in the mail or email saying 10% off because you're a valued member. But there's a trade off there of course.
06:42
It's not like the company, the other side of the coin there is that they're going to be tracking all of your information as well, right? Every time you make a purchase with that card they're seeing what you purchased, how much you spent, what you bought, what day it was and then they'll start using that information to target you.
07:00
So this is the kind of system that we'll be looking at. I got tired of e-commerce so that's why we're not doing that because it's just kind of boring. I'm not very creative so I had to do something that I've actually worked on so this is that. So the concepts we had in our system. One, we had the concept of a member. So there are shoppers that come into the store
07:22
that don't have a card and those people aren't members because they don't have a card. If you do have a card then you are a member and there's certain pieces of information we need from you. We've got things like your first name, your last name, email, we've got a list of assigned offers and we've also got this number,
07:40
the number of active offers. The reason why we got that special number is because whenever a user logs into the application, or not logs in the app, they go to the website and they log in with their accounts. It's linked in with their loyalty account so it'll show at the top a big number like you've got five active rewards, go spend money to go buy something right now. So it's just a gentle reminder to the user to go do that.
08:03
In this case we're tracking it explicitly because it was expensive to go calculate it every single time. I don't want to do a join and sum and things like that. So this is just a calculated value that we keep track of. And we just kept track of it on the member just to make it easy to display to users.
08:21
So I could show this to our business analysts and they would understand everything on here. They would say, oh yeah, the member, I don't know what an entity is but I'll assume that's some nerd thing. And then it's got some things on there like first name, last name, email, and those match. So the business called them last names, they didn't call them surnames.
08:41
If I called them surnames and they called them last names it would confuse them here. So I make sure and choose the names that they use. In this case they use the term last name. And they just call it email, they don't call it email address, they just say email. So that's what we call it in our system as well. And it has this list of assigned offers here, this collection.
09:02
In our system, whenever we give someone a offer, it's yours and yours alone and not anyone else's. So just like if I were to print some out and hand them out to everyone here, each physical piece of paper or coupon I hand out is yours and it's different from the other one I gave to someone else.
09:21
And in this system they could track them explicitly so they could do all sorts of downstream things. That's what we have in this case, a member has a list of a collection of assigned offers. The offer had the member assigned as part of it so we knew exactly who this offer was for.
09:44
We knew the type of offer it was, so perhaps a 10% off was different than the Christmas or New Year's or whatever are the holiday ones. We had different types of offers with different behaviors. Then of course we had the expiration date because these things don't live forever. They always got some date.
10:01
They want to make sure you're doing your spending. And then some value, $10, whatever it might be. Offer type has some information about it. This is just metadata information about an offer. So there may be only about 10 or 12 of these things configured in the entire system. They've got a name, they've got an expiration type.
10:21
So some of these are date bound. So for Christmas, for example, they say this offer's only good from December 1st to January 1st, and that's it. We have other ones though that are more behavioral based. So whenever you go spend $100, you get a $10 coupon,
10:40
but the clock starts ticking whenever we give it to you. So it's dynamic. So if I issued it to you today, it would be good until, let's see, what's today, the 6th? It would be good until the 16th. So different behavior for different kinds of offers. And the expiration type just describes what kind of expiration it is, and that's just a simple enum.
11:01
It's assignment based, and another is a fixed date. So I showed these classes to our business analyst, and they said, yeah, that looks good. All the names match. Even all the associations, they're named well. So I go look at things like between member and offer.
11:25
I don't just say offers, I say assigned offers. That's the kind of relationship there, and it describes the relationship between the offer and the member. It's not just a name of the thing. The same thing with member assigned. It describes the relationship.
11:42
So I've got all these classes, all these properties, even associations between these things, and lots of ORM tools will let you build this model really easily. If you ever use things like Entity Framework or things like that, they can actually drag and drop and build something like this. They'll actually build the tables and everything behind the scenes.
12:03
But there's something missing here. Does anyone notice what's missing? Methods. Right, everything I see here says properties, but there's nothing that says methods. So it looks rich
12:20
because I've got arrows pointing in different directions. The names all match things in our system, or in our model and in the business world. Even the relationships are named correctly. But again, these things don't have any behavior, so there's nothing governing how they're actually supposed to work. So if I look into behavior, if I actually go look for behavior in systems like this,
12:42
it becomes a bit of a wild goose chase to go look to see, something is manipulating these things, but where is that? Where do you all find it typically? Page load, button one underscore click, controller action, or helper classes.
13:04
We have a whole host of names for these things. We can go look for classes that are called something service, something helper, something manager. That's what I used to use the last decade, was managers, everything was a manager. So we find them over here.
13:20
Well, in our example, they're in the services folder. But they could be in the page behind, the code behind or whatever. They could be in the controller action. They could be in a special service helper class, or they could be in button click. It's somewhere because our application is doing something, but it's not in any of our domain model part.
13:43
Now, if you've read the big blue book from Eric Evans, he does describe this concept of services in this domain-driven design world, where you do have these helper classes to help coordinate activities between different domain model entities. And that's okay, that's perfectly reasonable.
14:02
What we don't want to have is all of our behavior in the application living in these services. Does anyone know the name for models like this? Anemic domain models.
14:21
So the idea behind, or the concept behind the term of anemic domain model is that at first blush, it looks like the real thing. The names of things match. Even the relationships might match. But once you peel off the first layer, you realize that there's no actual behavior behind these objects. They're a little more than property bags, and they're just a thin veneer,
14:40
a strongly-typed version over the database. It's just however easy it is to get a database row into an object, that's what we got. Now, there's nothing wrong necessarily with anemic domain models. In fact, most persistent object models are just that. The idea behind what differentiates
15:01
an anemic domain model versus just a application that doesn't really have behavior is that in this case, we're trying to do domain models, but we just haven't quite got there. Where systems that don't have behavior, they're just business objects or persistent objects, that's all they are.
15:21
So let's look at one of these services in our application that has all of our behavior, but nothing else. So in this case, we're looking at the use case of actually assigning an offer to a person. So this thing is called the offer assignment service. In this class, we want to see what it actually does, I go pop open the code.
15:41
I see that it has a number of constructor arguments because we're also smart, so we're using dependency injection and all that business. It looks like it's doing a few different things, but we look at where the behavior lies, it's actually down here in this assign offer. Where I might expect that to be just a few lines of code, it actually turns out it's several dozen lines of code
16:01
just to assign an offer to a person. It wouldn't seem like it'd be that hard to do because it's just a collection offers and then I go add the offer to the person and it should be done. Well, it started out that way. It started out just as like a one line, load things up and go move things over. So it started out something simple like this. It started out with, well, I need to get the things out of the database
16:20
so I'll use these repository objects to go load things out of the database. That pretty much any way I go, I'll have to do something like that. Well, then I have to calculate the value of the offer because maybe some customers get better offers than others. The business has decided that. They'll often tweak values of offers based on behavior.
16:41
So if you're a big spender, you actually won't get as many offers because hey, you're spending anyway, right? So why give you discounts? You're already spending. But if you're not a high spender, then we'll use higher value offers to try to get you back into the store. So we have something here to be able to do that. I don't know what it does. Maybe it calls some WCF service, who knows?
17:04
Then you have to calculate the expiration date. Originally, there were only date-based expirations where it was after it was given the ones that had a specific good for 10 days kind of thing.
17:20
Those didn't exist. It was just a, they had a begin date and end date and that was that. But this time I have two kinds. I have an assignment and a fixed-based one. So our friend the switch statement comes out to be able to do this kind of switching based on different ones. Now I've got the same switch statement, by the way, in like 20 other places in the application, just over and over again. Now that I've got those pieces of information,
17:42
now it comes time to actually build up the offer. I create an offer. I do the assign the offers.add offer. I increment the number of active offers and I save and everything's good. Now there's one piece in our application that caused a huge bug and lots of angry users
18:02
and it was around these two lines right here. So in our situation, when I inherited the application from someone else, I didn't realize how important this line of code was right there. Because in just one spot, it was with the other assigned offers. I didn't realize that I was supposed to be tracking that information.
18:21
So somewhere else we had some other process that went in assigned offers. I started getting complaints from members saying, hey, my number of offers doesn't match anymore. What's going on? It says I got two active offers but then I go to my system, it shows five. You're not telling me about these things that are happening. And it was because of this.
18:40
This requires us to remember to write this code. I have to remember to always increment that number of active offers. And if I don't remember, customers are going to be upset. What I found is, especially with systems with large number of users, in this case the system had about 10 million users in it, that as you start increasing the number of people, the number of crazies that come out of that percentage
19:02
start to really increase. So maybe the percentage of crazy people in the universe is like one out of every 10,000. But when you have millions of users, well that's why I guess you have help desk. And when you do something like this, it just completely infuriates lots of people. And that's why we started moving
19:20
towards actual domain models because we could no longer trust ourselves as developers to write good code all the time. That'd be something the tests would really help because I would have to know that I had to do that when I was writing the test. If I didn't know to do that, I wouldn't write the failing test to do so, so unit testing isn't really going to help me here. So for the rest of the presentation,
19:42
what we're going to do is take what we saw earlier, an anemic domain model and that service, and we're going to slowly refactor it until we actually get a true domain model that is responsible for its own operational behavior, it's responsible for encapsulating all the behavior around it, and also make sure that it'll do stupid things like this.
20:05
I'm going to switch over to Visual Studio. I'll be coding in C sharp. All right, is this large enough for everyone?
20:21
Yeah, that's great, okay, good. So this is just the same code I was showing earlier. We've got the assign offer, we've got repository junk, we've got a switch statement that, in this case is only in one spot but in our real system was like in 50 places. So let's have some fun here.
20:42
What if we were to do something like member.assignoffers.clear? There's nothing wrong with the interface that tells me I can't do that, right? I mean, the clear method is on that assign offers. What's stopping me from doing this besides not being an idiot?
21:04
Well, there's nothing stopping me because I look at what the object model gives me and there's nothing that tells me, oh, don't use that method, that's wrong. Now, if I were to show that to the business users, the business analysts, I mean, they would just freak out. You can't clear offers from a member. That's just taking everything away from them.
21:21
I mean, the emails they would get for clearing offers is just ridiculous. As I know, the only thing you can do is you can assign offers but you cannot ever take them away because I've already emailed them. We've already sent you an email saying, come get your offer and go use it in a store. Or maybe it's on the website and they've already printed it out and I take it away and now it's no good on the way to the store, it doesn't make any sense.
21:43
So the first thing I'm looking at doing is getting rid of, allowing anyone to do anything to the assigned offers. And the refactoring we'll use to fix this is the encapsulated collection refactoring from Martin Fowler's refactoring book.
22:02
I really don't have a crush on Martin Fowler, he's just written a lot of good books, I promise. So instead of us saying member.assignedoffers.add, if I remember back to the conversation with the business, they said, well, you can assign an offer to a member but you can't take them away. Well, right there they keyed me in,
22:20
I can assign an offer to a member. So how about I just say, assign offer. And I create a method that actually represents what I'm trying to do here. In that case, I'll say assign offer
22:40
and passing the offer there. I'll get rid of this one, and get rid of that guy too. And through the magic of resharper, assignedoffers.add offer.
23:05
Well, it's still not quite fixed things yet, right? I can still do things like member.assignedoffers.insert. Is there an insert? Nope, there's a copy to. I mean, that doesn't even mean anything.
23:21
How can I copy offers to another member? There's no button on any screen that allows anyone to do that. So why am I allowing my domain model to do this? So the way we can handle this is instead of exposing these things as a collection, why don't I expose them as an enumerable?
23:40
Because you can go over the list of them but otherwise I don't allow outside people to do anything else besides just enumerate them. You can add them, but you can't get at the inside part there. Now, I have to do a few things to help the internal part of here. So I have to go create this as a field
24:00
so that internally I can still access the collection. And this is a list now. But externally, I still don't want people to do things like, you know what, member.assignedoffers equals,
24:21
yeah, how about a new list of something? Why not? Just go ahead and blow everything away and we'll get an entirely new collection. Now in addition to something like our business being totally freaked out that we could do something like this, it also, it usually messes up all of our ORM, the object relational mapping stuff with the database. They don't like you to go just replace objects wholesale
24:42
with new things. It just, they don't work well with that. So, but yeah, we're allowing people to do this. So what we'll do is then get rid of that setter. Now no one can go ahead and set that. Internet wants me to do, yeah, read only, whatever.
25:05
All right, so what have I done here? I've just taken what used to be direct access to the collection and taken away all that access and saying, you only get to use only the methods that I allow you to use, just enumerating the collection. And I'm going to offer little windows
25:21
into functionality like assign offer. I will let you know what you can do. And what doesn't exist, you cannot do. What else? Let's see, the other thing we have here is,
25:43
I can do funny things like var member equals new member, and, oh, he's already down there, member two. Two dot assign offer, and then whatever.
26:05
So when I look at the first member, this member two object, if I show the business what that actually represents in our system, this represents anonymous no-name man with a bag on his head walking into the store. He's got no name, no email,
26:21
he's just completely anonymous. So we asked, hey, business, can you have just anonymous people coming in and becoming a member? Well, no, that's the trade-off here. If you're going to become a member in our program, we're going to give you coupons, you got to give us personal information about you so that's, well, we can track your stuff and do all sorts of other nefarious things.
26:43
But right now I'm looking at this and it said there's nothing that tells me, as a developer, that member requires certain information. There's nothing on that object that says, well, when you create one of these guys, you have to have a first name, you have to have a last name, you have to have an email. You have to have these things for it to be a real member.
27:02
These are what's known as invariants. These are the pieces of information on a class, on an object, that taking any one of those things away it no longer represents that concept. So a zebra without stripes is what? It's a horse.
27:20
An albino zebra. I don't know if those exist, but that would be a subclass of zebra, the albino zebra. So you take its tail away, is it still a horse? If you take its hooves away, its legs away, is it still a horse? I mean, it's kind of demeaning to visualize that,
27:41
but you get the idea. So the idea here is that I have something that's called a member, yet I'm not forcing it to have certain required information in order for it to exist in the world. So if we wanted to represent that in code, we want to say, well, you have to have these pieces of information for it to exist in the world? What do we use for that?
28:03
Constructors, right? That's telling the developer, here's what's required for you to construct one of these objects. So instead of us saying, well, you don't need the first name, last name, email, you can just have a know our constructor. Let's take that away and say, you actually have to have this information in here.
28:24
So I'm going to generate a constructor with the first name, last name, and email, and I will get rid of my old one. So no longer in my system do I allow anyone to be able to create one of these objects
28:43
without this certain set of information. And where we see this really come up are things like a full name property that concatenates things together and assumes that information is always there. So say, well, you know in real life, there's a screen that has validation rules, so it's always going to be like that.
29:00
But I as a developer don't know that, so something like this constructor, a nice little breadcrumbs that says, this is how the object is supposed to behave, this is what it requires. At this point, if you have a lot of unit tests, this is where you may want to not go any further, because it's usually at this point where I get about one or two compile errors in my system
29:24
and about 1,000 in my unit tests. Because my unit tests are like, yeah, I'll create people without names, I don't care about that right now. So in those cases, you may want to do something like leaving a know our constructor around, or just go ahead and take care of the compile errors. That's usually the route I go, and it's less work than it turns out to be.
29:43
I think the record I've had for compile errors for doing something like this was something like 1,300. And at that point, I think the Visual Studio compiler just stopped reporting them. And it took about three hours for me to actually get rid of them all. It was a long and tedious three hours, but it was far less than the week I really thought it was going to be,
30:01
like 1,300 compile errors. Ugh, it's going to take all week to do this. It wasn't too bad. So I promise you, it can be worth it, and it's not as work as you may think. All right, so, well, that doesn't actually happen here. So looking at something we do construct here, we are constructing our offer.
30:21
So if I look at this and I say, what pieces on the offer object could I take away and it still be an offer? Maybe. I could ask the business and say, have you ever just given out these coupons just to people on the street?
30:41
And you don't care who they're given to? Maybe, maybe not. For our application, they did have those kinds of offers in the system where they weren't actually assigned to people, but those turned out to be not tracked in our system, and for us, every offer was assigned to a specific member
31:02
and the other ones were just taken care of by someone else. So they come back to us and they told us, it actually has to have all of this information on here. If I take anything away, like take away the expiration date, so you have offers that are good forever? No. Okay, so that's required. The value, do you have any values where
31:22
you give them a coupon and they're allowed to just write in whatever they want there? Like, yeah, sure, let's make the value 11y billion. Why not? Well, no. So I look at this and they say, well, all these are required, so let's make them all required. So I make a constructor that requires all this information.
31:42
And of course, in our service, we'll have to change it to be able to do that other. So I got member, offer type, that, that, that wasn't too bad.
32:03
With me so far? So we haven't gotten yet to the real bug we ran into, which was around the number of active offers that said, by the by, by the by, make sure you go ahead and increment that number any time you change anything in the offers.
32:22
Well, we've already constrained what people can do with those offers. I've no longer allowed people to go straight to that collection object so they can just do whatever they want to it. And I've already created a method called AssignOffer to represent the operation of assigning offers, so why don't we just move this task of updating the number of active offers
32:41
and just move that to the AssignOffer method. So now you're starting to see a little bit more behavior going on. I'm keeping track of the number of active offers internally as part of the AssignOffer method on my member object.
33:11
Well, that compiles.
33:21
So I have this weird middle state now. Am I supposed to be keeping track of the number of active offers myself, or is the member object supposed to be doing that for me? Which is it? Well, if I have a public setter here, then it's telling everyone, do what you will with me. Have your way with me.
33:43
Objects like this that have critical information that is exposed with public setters and anyone can do anything what they want with them, those have been given a couple terms. One is promiscuous objects that let anyone do anything they want with them. I'm sure there are some more politically correct names, but that's one I like to use.
34:02
There are some less politically correct ones as well, so I can share those afterwards. So what do we do? We just take away the ability for people outside this object to be able to go and mess with that information by making a private setter. Now I haven't broken,
34:20
I have not broken a single line of production code right now because I've moved the number of active offers manipulation inside that assignOffer method and nowhere else in my application was it doing this except maybe unit tests. Because unit tests, maybe I was just doing whatever I want there to get my object to be where I want it to be, but now those all broke.
34:41
But typically when I start doing these refactorings, I don't usually break production code, I usually just break tests. And then also sometimes keys me into good tests, bad tests. You may do this with the other ones as well, the first name, last name, email saying,
35:01
well I want to encapsulate setting the first name because I need to check to make sure it's not null. That's whatever. When I look at this, the one that has caused me bugs is number of active offers. So that's the one I'm going to wrap up and make sure that I don't allow it to break in the future.
35:23
So the principle behind that is just encapsulation. I'm encapsulating keeping track of that behind the person class. I'm not allowing everyone else or not even just allowing. I'm removing the responsibility from other people to have to keep track of that information.
35:44
All right, let's keep going. So, okay, I do get one compiler here because I moved that out. So let's delete that line of code there. Let's see, how about this?
36:01
I create an offer and I assign it a value of $11 billion, valid, compiles. But conceptually, that's not what our business was looking for. Now look at this offer saying that, hey, just give me a number in here
36:20
and I will do whatever I want with it. So what the business is asking us to do though is they're asking us to say, you must go through this offer value calculator to calculate the value for an offer and you can't use anything else. You must use this guy. This is a little tricky though. I can't really make my offer depend on that surface.
36:42
I can't really inject it into the entity. I can't really inject it into the member. Maybe you can, but you should not. But as part of creating that offer, I need to be using the value calculator to actually do that. So let's do a couple things here.
37:03
One, I want to move the creation of the offer inside the assign offer method. And the reason for this is because I want to make sure that everything that happens as far as assigning the offer is done correctly and I don't miss anything. For example, all of the pieces where I had as far as making sure that both directions were correct
37:23
and things like that, I want to make sure I don't do something weird like, you know, new member. I want to be able to do that where I say, I can accidentally assign this offer to another member. And the only way I can really enforce that is by moving the creation of the offer inside the assign offer method.
37:41
So what I'm going to do is, this is going to now return an offer and the creation of it is going to be done inside this method.
38:01
So no longer will we just take in the offer. What I will need though is the offer type, offer type. I will need the date expiration date, date expiring. And I also need the value. And now my offer is created not with some member
38:22
that I don't have any control over, it's created with me. So when I create the offer, I'm creating with myself and this other information. So I actually don't even allow, in this case, someone to accidentally assign an offer to another member. I'm encapsulating all that directly inside here.
38:42
And the last thing I need is to actually return the offer. Now, the only reason I'm returning the offer in this case is because perhaps the data access layer I have requires me to explicitly save every single object that's new. But if you're using something,
39:01
one of the more advanced ORMs out there, like Hibernate or InHibernate, it'll automatically be able to track those child objects. We don't have to do the step of, oh, and go save it by the way. So if I were actually using InHibernate, I could get rid of this right here and get rid of the save. And it would just automatically save it
39:20
because it's belonging to the member collection. All right, but we started with this down this path because I was allowing anyone to assign any value at all into the value here. But what I really want to do as part of assigning an offer is say,
39:40
when you assign an offer, you must use this offer value calculator. So how about instead of me calculating the value right here, how about instead I pass it something that can do that work for me? So instead of saying, I'm gonna pass you the value directly, I'll pass it something that can do that.
40:04
So instead of having a pre-calculated value being passed in, it'll have an IOffer, what was the name of that, value calculator, offer value calculator. And then now the value is going to be the offer value calculator.calculate value,
40:20
passing in myself and the offer type. And now back in my service, I no longer need to calculate the value ahead of time. Instead, my assignOffer method says, hey, if you want to assign an offer that's going to have a value, you must give me something that's going to calculate that because I'm not going to try to do it myself.
40:42
So we pass it the offer value calculator. The offer value calculator then passes whatever it needs out to be able to calculate that information. And then that's where my value comes from. Now the benefit of this is that I explicitly tell people using this code, this is the thing I need to use
41:02
to be able to calculate this information. The downside is it's a little funny passing in these service objects to domain object methods. But to me, the benefits outweigh the liabilities here and that I'm being very explicit about where this value comes from as opposed to, eh, just a number. Just give me something, I don't care what.
41:21
I don't care where it comes from, just give me a number. Okay, I've saved the ugliest for last. Is there anything else we can do with assign offer to make this cleaner?
41:44
Anything else bothering anyone about this method? It's not a trick question, I was just curious. Oh yeah, that's a whole other thing. The question was, or the statement was,
42:03
nothing right now is actually taking into account expirations, you're right. And if this were to everything, then maybe we could go into expirations, but since it's only one hour, we'll only look at assignments. But yeah, something else, you can imagine that there's expirations here that also have to deal with decrementing that number as well.
42:23
I could go and look at offer type, for example, and ask the same questions. Is there anything on here that's not required? No. Okay then, so let's go ahead and say, go ahead and give me everything on here. You must supply all information to create a valid offer type. There's no such thing as a blank offer type
42:41
with no value, no sort of expiration, no name that doesn't exist in our system. So by giving a constructor here, we're again just kind of hardening up our model to say what's required. So I think everything in our system now has the minimum set of information needed for it to be that thing.
43:04
So the last part in our system is this bad boy. We could move some of this around. We could say, well, instead of me passing you an expiration date, do you really need that? Can you just calculate it yourself?
43:20
I think so. So let's try that first. Let's see if that makes things cleaner. I'm going to take this switch statement and move it down into my assign offer method.
43:41
There we go. So at this point, my assign offer method for my service is about as small as it can be. I load something up from a database, load another thing up from the database, and one method to do everything. And when I look at that method, it actually makes more sense about
44:00
what the user is trying to do. I'm trying to say, assign an offer of this type and use this to calculate the value. And that encapsulates the entire operation of assigning an offer. But I still haven't really solved my problem of this nasty, ugly switch statement. Now in this application, in our example,
44:22
we've only got one switch statement, but typically with something like this, there's one for the user interface, one for calculating the actual expirations versus the assignments. In this case, there was something like one or 200 usages of this as a switch statement to do different things based on different whatever.
44:44
But the problem we have is that we don't have much recourse here. Typically I would say, let's go ahead and push this behavior down into the offer type class. But it's not a class, it's an enum. Does anyone here Java background?
45:02
Few of us. So you're actually lucky, you have real enumeration types in your language. Where .NET has, and C Sharp specifically, has, well I guess it's a data thing, has rather lame enumerations. These enumerations are really no more than named integers, named numbers.
45:22
But they can't really have behavior. That's a huge drawback to using these enums. It's nice that they get design time stuff, they get drop downs and all these nice things. But other than that, the behavior around enums is littered around switch statements and if statements everywhere. So if I want to be able to say, let's push behavior down into this expiration type,
45:45
I have to go to something else. And then something else we're going to do is we're going to try to mimic Java based enumerations with our own enumeration base class.
46:02
This enumeration base class has a lot of the same things from the enum type in .NET to get things by name and all that kind of stuff. But what we're going to do is change our expiration types to be, instead of something that says, I just enums with a fixed value, I'm instead going to expose public static
46:22
read only fields with the same name and its expiration type. And it's going to be a new expiration type and I need to create a constructor with that information.
46:44
And we're going to make it private too, because again, I don't want to allow anyone just to go create new expiration types willy nilly. It's an internal thing only. So this guy has two arguments. That is the value, which is the number one. And I need a display name just for what is actually displayed to the end user.
47:02
In that case, it's just assignment. And I'll create another one for the fixed value one. So in the use of this, I do have one drawback is that I can't use switch statements anymore. I can only use if statements.
47:23
So instead of a switch offer type expiration type, I have to say if offer.expiration type equals assignment, do this, else if, and do the same thing.
47:41
I promise this is only temporary. We're going to make it better. Fixed, then we do this stuff, else, I guess it's argument out of range exception. But I really don't need to do that anymore because I've explicitly defined that. One of the weird things about enums, I don't know if you knew this about enums, is that they're not very strict about their values.
48:04
So I could do really weird things like enum foo. I could do really weird things like foo foo equals zero.
48:24
That's not a value, right? Zero's not a valid value. Well, in the land of enums, it is. John Skeet had a presentation a couple months ago. The John Skeet of Stack Overflow fame,
48:40
who had a presentation about the biggest missing pieces or mistakes of C Sharp. At least the presentation he gave that day, the one he described was this, that there are no real good enums in C Sharp. There are these things that are really bizarre, and his presentation went into all the kind of WTF stuff of enums,
49:00
that they're just really, really bizarre. But otherwise, we can't really use the enums out of the box to do really interesting things. Date expiring equals this, else, if, there we go.
49:22
And I'll ignore the other else case of not having one, because we'll take care of that another way. And the last case is throw new not,
49:43
what is it, not supported exception, not implemented exception, or just exception, whatever. And we'll do a Scooby-Doo exception, front row.
50:02
Okay, so I haven't really improved anything. I actually took a step back, because I went from this really nice switch statement, I go switch tab with V Sharp, and it spits out a nice switch statement. But it's still got these if statements, if this, that, if this, that, if this, that. But the thing I did gain from this is that I have something that actually does look rather like enumerations,
50:23
in that I go expiration type dots, and I got a list of valid values that only these exist and none others. I can't create a new expiration type externally. I can't just say, sure, var jimmy's expiration equals new expiration type.
50:40
And this one is awesome, it's val forever. Oh, but wait, I can't, I can't access that constructor. It's a private constructor. Private meaning only that type can access the constructor, which is being used here and here, but no one else can. So again, I'm, oops.
51:08
Well now that I have a real type that I can work with, I can start pushing this behavior down. So instead of saying, well let's make the assign offer method responsible for this, how about we make the offer type
51:20
object responsible for this? Now the reason why I know that offer type is the right object to use for this, because if I look at this method here, it says offer type, offer type equals this. If offer type is this, then do something with offer type. Offer type, offer type, offer type.
51:42
Lots of offer type. So look at this code. I'm only talking about offer type. I'm not talking about anything else. It's just concerned with offer type and that's it. So when you have these cases where you have one class really concerned with another one,
52:02
that's the code smell of inappropriate intimacy. And the way we can fix that is just moving that behavior to that other class. That's all we have to do. Easy, right? So let's take this. I'm going to do date time, date expiring
52:21
equals offer type.calculateExpiration. And we'll figure out what we need to actually pass in there in a second. But see now that I have a real class I can work with on expiration type, offer type, yeah here we go.
52:43
Now that I have a real type to work with, I have a place for that behavior. So instead of me going off to some other offer type, I just talk about myself now. So everywhere I saw offer type dot blah, I'll do this. Just get rid of that and now I'm only looking at myself.
53:06
And finally, now return new day time, that's silly. Return date expiring. Again, all I'm doing is just, back to my memory object, I'm just pushing the behavior down.
53:21
I'm saying offer type, you've got all the information you need to calculate the expiration, just go ahead and do that for me. Appreciate it. But I still haven't gotten rid of my switch statement. Where did I put that? Wrong place.
53:43
Sorry, I didn't mean to confuse anyone. It is supposed to be on the, too many types here. So if this equals that one or this equals that one, then okay, now we need to actually get the days valid.
54:03
Oh wait, I didn't have the right spot, I'm sorry. Get rid of this. And what I wanted to push down was from offer type, I wanted to move down the expiration date calculation
54:22
down to the individual expiration type class. And the things it needs are the days valid and that's really it, right? Days valid and begin date. So I could say something like, how about you do this, how about date expiring equals expiration type
54:41
dot calculate expiration, and I'll just pass it in myself. So whatever you need to do to get from me to be able to calculate expiration, go right ahead. So again, I'm gonna move all this now to my expiration type class.
55:03
And so now I'm just looking at this. This, our date expiring, date time, date expiring, and then return date expiring. And then now I'm looking at the offer type,
55:22
days valid, and the offer type begin date. And it's telling me I can get rid of this stuff. So if I'm an assignment kind of expiration, do this. Or if I'm a fixed type expiration, do that. So I pushed down pretty much as far as I can.
55:42
The calculation of the expiration date from offer type, which from the outside world on the member side, it just said, calculate expiration. I don't care how you do that or what you need, go take care of it. Offer type says, okay, I'll calculate it, but I actually need to delegate some things to the expiration type because there's different kinds.
56:00
And I'll let them use myself as backup to see any additional information. So this is, in the DDD world, domain-driven design world, this is called double dispatch. It's not the same thing as C++ double dispatch, I know. But the idea is that I pass in myself to another object who is then going to call back to me for certain information.
56:22
And that's what I'm doing here. I'm calling back into offer type to get information, and offer type has passed in itself to be able to call back into it. Okay, long road, still got this, well, it used to be a switch statement, but now it's just an ugly version of a switch statement.
56:43
The way we get rid of switch statements with refactoring is through polymorphism. What I want to do now is take each instance here and replace each instance with a subtype for that specific kind of calculation. So let's walk through that.
57:00
How about instead of a calculate expiration method that's just an if statement, how about I have public abstract date, time, calculate expiration, okay? Now I'm going to have two versions of this, one for the assignment kind and one for the fixed kind.
57:22
So I'm going to create a private class that is for assignment, and it's going to, assignment type, inherit from my expiration type,
57:41
and this guy is going to need to be protected, because I need to get access to that. And he needs a constructor, and he's just going to call the base now with whatever I passed in earlier up here.
58:01
So he's going to have these two things. That's, and he no longer needs any arguments in his constructor whatsoever. So this guy, instead of being just new expiration type, whatever, he's actually going to become new assignment type.
58:25
Oh, and it needs to be public. There we go. And this class now has to be abstract, yes.
58:41
Okay, so now in my method on the assignment type, I can now implement that one member for calculating expiration. And for assignment base, it's just this one line here. So I say return that, and I'll do another one for the fixed type.
59:01
Fixed type. And it's got a value of that and a value here of that. And now it's calculated expiration is going to be this guy down here. You're going to return that.
59:24
And now this guy goes away. And finally, I need to say use the correct expiration type object up here. So this guy is going to be the, what is that called, a fixed type? Fixed type.
59:41
And now my switch statement and all those if statements disappeared. For each individual case I have, I have a specific subtype for that case, assignment fixed, and then I just override the method on my base class for that specific behavior. So I've replaced, the specific refactoring here is, I'm replacing
01:00:01
case statements or switch statements with polymorphism. Each specific derived class has a behavior for that one piece, and it just fills in the hole. My switch statement, actually, they didn't have to care about what about values that don't exist. We don't have to care about that anymore, because these are the only ones that can possibly exist.
01:00:21
I still get the same enumeration kind of behavior. I have expiration type dot fixed, and I have that enumeration kind of thing where it has a fixed list of values. Now that it's in an object, an actual type, a class,
01:00:41
well now I can put methods on there and have the actual behavior. With subclasses, I get rid of all my switch statements because I can just put any derived behavior for each specific one on here. From the outside world, they actually don't even know how I do things behind the scenes. These assignment type and fixed type, those are private classes. No one even knows about these things
01:01:01
outside of expiration type, so it's completely encapsulated all the behavior around individual expiration types, something that .NET enums just cannot do. Look where we started. We've gotten our assign offer method down,
01:01:20
basically just get stuff out of the database and then delegate everything else to the domain model. Whatever it needs to do to perform that operation, just keep pushing behavior down. For me, that's what domain-driven design is all about. It's about building domain models that encapsulate the behavior. I'm not hiding behavior. I'm just saying it's up to the domain model itself
01:01:41
to perform all those operations itself. It's going to take care of its own. It defines its boundaries. It does not let anyone do whatever it wants. It wraps up everything nicely in a nice, neat bow. This is how I was able to eliminate bugs in our application a lot more easily than just writing a whole bunch of tests. Writing a bunch of tests still requires me
01:02:01
to know to write those tests, but in this case, the domain model is offering me the right path, so this is the way you need to go. The nice thing is we don't have to start out with there. We can start with the anemic domain model that's just property bags and just standard refactoring techniques we can move towards a true domain model.
01:02:21
That's why I never really worry about, oh my gosh, my domain model is horrible. Just keep refactoring until it is, and that's all there is to it. Thank you all very much. If you all have any questions, I'll be hanging around afterwards. Otherwise, have a great day. Thank you.