You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
And we could keep hacking about and adding extra lines to that `load_batches` function,
172
-
and some sort of way of tracking and saving new allocations—but we already have a model for doing that! It's called our Repository and our Unit of Work patterns.
172
+
and some sort of way of tracking and saving new allocations—but we already have a model for doing that! It's called our Repository and Unit of Work patterns.
173
173
174
174
All we need to do ("all we need to do") is reimplement those same abstractions, but
175
175
with CSVs underlying them instead of a database. And as you'll see, it really is relatively straightforward.
@@ -178,10 +178,10 @@ with CSVs underlying them instead of a database. And as you'll see, it really is
178
178
=== Implementing a Repository and Unit of Work for CSVs
179
179
180
180
181
-
Here's what a CSV-based repository could look like. It abstracts away all the
181
+
Here's what a CSV-based repository could look like.((("repositories", "CSV-based repository"))) It abstracts away all the
182
182
logic for reading CSVs from disk, including the fact that it has to read _two
183
-
different CSVs_, one for batches and one for allocations, and it just gives us
184
-
the familiar `.list()` API, which gives us the illusion of an in-memory
183
+
different CSVs_ (one for batches and one for allocations), and it gives us just
184
+
the familiar `.list()` API, which provides the illusion of an in-memory
185
185
collection of domain objects:
186
186
187
187
[[csv_repository]]
@@ -235,12 +235,12 @@ class CsvRepository(repository.AbstractRepository):
235
235
// TODO (hynek) re self._load(): DUDE! no i/o in init!
236
236
237
237
238
-
And here's what a UoW for CSVs would look like:
238
+
And here's((("Unit of Work pattern", "UoW for CSVs"))) what a UoW for CSVs would look like:
239
239
240
240
241
241
242
242
[[csvs_uow]]
243
-
.A UoW for CSVs: commit = csv.writer. (src/allocation/service_layer/csv_uow.py)
243
+
.A UoW for CSVs: commit = csv.writer (src/allocation/service_layer/csv_uow.py)
244
244
====
245
245
[source,python]
246
246
----
@@ -266,11 +266,11 @@ class CsvUnitOfWork(unit_of_work.AbstractUnitOfWork):
266
266
267
267
268
268
And once we have that, our CLI app for reading and writing batches
269
-
and allocations to CSV is pared down to what it should be: a bit
269
+
and allocations to CSV is pared down to what it should be—a bit
270
270
of code for reading order lines, and a bit of code that invokes our
271
271
_existing_ service layer:
272
272
273
-
273
+
[role="nobreakinside less_space"]
274
274
[[final_cli]]
275
275
.Allocation with CSVs in nine lines (src/bin/allocate-from-csv)
276
276
====
@@ -289,7 +289,7 @@ def main(folder):
289
289
====
290
290
291
291
292
-
Ta-da! _Now are y'all impressed or what_?
292
+
Ta-da! _Now are y'all impressed or what_?((("CSVs, doing everything with", startref="ix_CSV")))
@@ -160,7 +162,7 @@ help minimize boilerplate for this sort of thing.]
160
162
161
163
==== Custom Methods on Django ORM Classes to Translate to/from Our Domain Model
162
164
163
-
Those custom methods look something like this:
165
+
Those custom methods ((("object-relational mappers (ORMs)", "Django, custom methods to translate to/from domain model")))((("domain model", "Django custom ORM methods for conversion")))look something like this:
164
166
165
167
[[django_models]]
166
168
.Django ORM with custom methods for domain model conversion (src/djangoproject/alloc/models.py)
@@ -209,7 +211,7 @@ class OrderLine(models.Model):
209
211
210
212
<1> For value objects, `objects.get_or_create` can work, but for entities,
211
213
you probably need an explicit try-get/except to handle the upsert.footnote:[
212
-
`@mr-bo-jangles` suggested you might be able to use https://oreil.ly/HTq1r[`update_or_create`]
214
+
`@mr-bo-jangles` suggested you might be able to use https://oreil.ly/HTq1r[`update_or_create`],
213
215
but that's beyond our Django-fu.]
214
216
215
217
<2> We've shown the most complex example here. If you do decide to do this,
@@ -220,14 +222,14 @@ class OrderLine(models.Model):
220
222
221
223
222
224
NOTE: As in <<chapter_02_repository>>, we use dependency inversion.
223
-
The ORM (Django) depends on the model, and not the other way around.
225
+
The ORM (Django) depends on the model and not the other way around.((("Repository pattern", "with Django", startref="ix_RepoDjango")))((("Django", "Repository pattern with", startref="ix_DjangoRepo")))
224
226
225
227
226
228
227
229
=== Unit of Work Pattern with Django
228
230
229
231
230
-
The tests don't change too much:
232
+
The tests((("Unit of Work pattern", "with Django", id="ix_UoWDjango")))((("Django", "Unit of Work pattern with", id="ix_DjangoUoW"))) don't change too much:
@@ -309,7 +311,7 @@ class DjangoUnitOfWork(AbstractUnitOfWork):
309
311
====
310
312
311
313
<1> `set_autocommit(False)` was the best way to tell Django to stop
312
-
automatically committing each ORM operation immediately, and
314
+
automatically committing each ORM operation immediately, and to
313
315
begin a transaction.
314
316
315
317
<2> Then we use the explicit rollback and commits.
@@ -318,13 +320,13 @@ class DjangoUnitOfWork(AbstractUnitOfWork):
318
320
instrumenting the domain model instances themselves, the
319
321
`commit()` command needs to explicitly go through all the
320
322
objects that have been touched by every repository and manually
321
-
update them back to the ORM.
323
+
update them back to the ORM.((("Unit of Work pattern", "with Django", startref="ix_UoWDjango")))((("Django", "Unit of Work pattern with", startref="ix_DjangoUoW")))
322
324
323
325
324
326
325
327
=== API: Django Views Are Adapters
326
328
327
-
The Django _views.py_ file ends up being almost identical to the
329
+
The Django _views.py_ file ends ((("views", "Django views as adapters")))((("adapters", "Django views")))((("Django", "views are adapters")))((("APIs", "Django views as adapters")))up being almost identical to the
328
330
old _flask_app.py_, because our architecture means it's a very
329
331
thin wrapper around our service layer (which didn't change at all, by the way):
330
332
@@ -370,7 +372,7 @@ def allocate(request):
370
372
=== Why Was This All So Hard?
371
373
372
374
OK, it works, but it does feel like more effort than Flask/SQLAlchemy. Why is
373
-
that?
375
+
that?((("Django", "using, difficulty of")))
374
376
375
377
The main reason at a low level is because Django's ORM doesn't work in the same
376
378
way. We don't have an equivalent of the SQLAlchemy classical mapper, so our
@@ -382,7 +384,7 @@ high).
382
384
Because Django is so tightly coupled to the database, you have to use helpers
383
385
like `pytest-django` and think carefully about test databases, right from
384
386
the very first line of code, in a way that we didn't have to when we started
385
-
out with our pure domain model.
387
+
out with our pure domain model.((("pytest", "pytest-django plug-in")))
386
388
387
389
But at a higher level, the entire reason that Django is so great
388
390
is that it's designed around the sweet spot of making it easy to build CRUD
@@ -397,17 +399,16 @@ around the workflow of state changes. The Django admin bypasses all of that.
397
399
=== What to Do If You Already Have Django
398
400
399
401
So what should you do if you want to apply some of the patterns in this book
400
-
to a Django app? We'd say the following:
402
+
to a Django app?((("Django", "applying patterns to Django app"))) We'd say the following:
401
403
402
404
* The Repository and Unit of Work patterns are going to be quite a lot of work. The
403
405
main thing they will buy you in the short term is faster unit tests, so
404
-
evaluate whether that feels worth it in your case. In the longer term, they
406
+
evaluate whether that benefit feels worth it in your case. In the longer term, they
405
407
decouple your app from Django and the database, so if you anticipate wanting
406
408
to migrate away from either of those, Repository and UoW are a good idea.
407
409
408
410
* The Service Layer pattern might be of interest if you're seeing a lot of duplication in
409
-
your _views.py_. It can be a good way of thinking about your use cases,
410
-
separately from your web endpoints.
411
+
your _views.py_. It can be a good way of thinking about your use cases separately from your web endpoints.
411
412
412
413
* You can still theoretically do DDD and domain modeling with Django models,
413
414
tightly coupled as they are to the database; you may be slowed by
@@ -422,25 +423,24 @@ https://oreil.ly/Nbpjj[word
422
423
in the Django community] is that people find that the fat models approach runs into
423
424
scalability problems of its own, particularly around managing interdependencies
424
425
between apps. In those cases, there's a lot to be said for extracting out a
425
-
business logic or domain layer to sit between your views and forms, and
426
+
business logic or domain layer to sit between your views and forms and
426
427
your _models.py_, which you can then keep as minimal as possible.
427
428
428
429
=== Steps Along the Way
429
430
430
431
Suppose you're working on a Django project that you're not sure is going
431
432
to get complex enough to warrant the patterns we recommend, but you still
432
433
want to put a few steps in place to make your life easier, both in the medium
433
-
term, and if you want to migrate to some of our patterns later. Consider the following:
434
+
term and if you want to migrate to some of our patterns later.((("Django", "applying patterns to Django app", "steps along the way"))) Consider the following:
434
435
435
-
* One piece of advice we've heard is to put a __logic.py__ into every Django app,
436
-
from day one. This gives you a place to put business logic, and to keep your
436
+
* One piece of advice we've heard is to put a __logic.py__ into every Django app from day one. This gives you a place to put business logic, and to keep your
437
437
forms, views, and models free of business logic. It can become a stepping-stone
438
438
for moving to a fully decoupled domain model and/or service layer later.
439
439
440
-
* A business-logic layer might start out working with Django model objects,
441
-
and only later become fully decoupled from the framework and work on
440
+
* A business-logic layer might start out working with Django model objects and only later become fully decoupled from the framework and work on
442
441
plain Python data structures.
443
442
443
+
[role="pagebreak-before"]
444
444
* For the read side, you can get some of the benefits of CQRS by putting reads
445
445
into one place, avoiding ORM calls sprinkled all over the place.
446
446
@@ -449,10 +449,10 @@ term, and if you want to migrate to some of our patterns later. Consider the fol
449
449
concerns will cut across them.
450
450
451
451
452
-
NOTE: We'd like to give a shoutout to David Seddon and Ashia Zawaduk for
453
-
talking through some of the ideas in this chapter. They did their best to
452
+
NOTE: We'd like to give a shout-out to David Seddon and Ashia Zawaduk for
453
+
talking through some of the ideas in this appendix. They did their best to
454
454
stop us from saying anything really stupid about a topic we don't really
455
455
have enough personal experience of, but they may have failed.
456
456
457
-
For more thoughts and actual lived experience dealing with existing
458
-
applications, refer to the <<epilogue_1_how_to_get_there_from_here>>.
457
+
For more ((("Django", startref="ix_Django")))thoughts and actual lived experience dealing with existing
458
+
applications, refer to the <<epilogue_1_how_to_get_there_from_here, epilogue>>.
Copy file name to clipboardExpand all lines: appendix_ds1_table.asciidoc
+5-5Lines changed: 5 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
[appendix]
3
3
== Summary Diagram and Table
4
4
5
-
Here's what our architecture looks like by the end of the book:
5
+
Here's what our architecture looks((("architecture, summary diagram and table", id="ix_archsumm"))) like by the end of the book:
6
6
7
7
[[recap_diagram]]
8
8
image::images/apwp_aa01.png["diagram showing all components: flask+eventconsumer, service layer, adapters, domain etc"]
@@ -20,7 +20,7 @@ image::images/apwp_aa01.png["diagram showing all components: flask+eventconsumer
20
20
__Defines the business logic.__
21
21
22
22
23
-
| Entity | A domain object whose attributes may change, but that has a recognizable identity over time.
23
+
| Entity | A domain object whose attributes may change but that has a recognizable identity over time.
24
24
25
25
| Value object | An immutable domain object whose attributes entirely define it. It is fungible with other identical objects.
26
26
@@ -34,7 +34,7 @@ __Defines the business logic.__
34
34
35
35
__Defines the jobs the system should perform and orchestrates different components.__
36
36
37
-
| Handler | Receives a command or event and performs what needs to happen.
37
+
| Handler | Receives a command or an event and performs what needs to happen.
38
38
| Unit of work | Abstraction around data integrity. Each unit of work represents an atomic update. Makes repositories available. Tracks new events on retrieved aggregates.
39
39
| Message bus (internal) | Handles commands and events by routing them to the appropriate handler.
40
40
@@ -50,9 +50,9 @@ to the outside world (I/O).__
50
50
51
51
__Translate external inputs into calls into the service layer.__
52
52
53
-
| Web | Receives web requests and translates them into Commands, passing them to the Internal Message Bus.
53
+
| Web | Receives web requests and translates them into commands, passing them to the internal message bus.
54
54
| Event consumer | Reads events from the external message bus and translates them into commands, passing them to the internal message bus.
55
55
56
-
| N/A | External message bus (message broker) | A piece of infrastructure that different services use to intercommunicate, via events.
56
+
| N/A | External message bus (message broker) | A piece of infrastructure that different services use to intercommunicate, via events.((("architecture, summary diagram and table", startref="ix_archsumm")))
0 commit comments