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.
@@ -180,8 +180,8 @@ with CSVs underlying them instead of a database. And as you'll see, it really is
180
180
181
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]]
@@ -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_?((("CSVs, doing everyting with", startref="ix_CSV")))
292
+
Ta-da! _Now are y'all impressed or what_?((("CSVs, doing everything with", startref="ix_CSV")))
@@ -209,7 +207,7 @@ class OrderLine(models.Model):
209
207
210
208
<1> For value objects, `objects.get_or_create` can work, but for entities,
211
209
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`]
210
+
`@mr-bo-jangles` suggested you might be able to use https://oreil.ly/HTq1r[`update_or_create`],
213
211
but that's beyond our Django-fu.]
214
212
215
213
<2> We've shown the most complex example here. If you do decide to do this,
@@ -220,7 +218,7 @@ class OrderLine(models.Model):
220
218
221
219
222
220
NOTE: As in <<chapter_02_repository>>, we use dependency inversion.
223
-
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")))
221
+
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
222
225
223
226
224
@@ -309,7 +307,7 @@ class DjangoUnitOfWork(AbstractUnitOfWork):
309
307
====
310
308
311
309
<1> `set_autocommit(False)` was the best way to tell Django to stop
312
-
automatically committing each ORM operation immediately, and
310
+
automatically committing each ORM operation immediately, and to
313
311
begin a transaction.
314
312
315
313
<2> Then we use the explicit rollback and commits.
@@ -382,7 +380,7 @@ high).
382
380
Because Django is so tightly coupled to the database, you have to use helpers
383
381
like `pytest-django` and think carefully about test databases, right from
384
382
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.
383
+
out with our pure domain model.((("pytest", "pytest-django plug-in")))
386
384
387
385
But at a higher level, the entire reason that Django is so great
388
386
is that it's designed around the sweet spot of making it easy to build CRUD
@@ -401,13 +399,12 @@ to a Django app?((("Django", "applying patterns to Django app"))) We'd say the f
401
399
402
400
* The Repository and Unit of Work patterns are going to be quite a lot of work. The
403
401
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
402
+
evaluate whether that benefit feels worth it in your case. In the longer term, they
405
403
decouple your app from Django and the database, so if you anticipate wanting
406
404
to migrate away from either of those, Repository and UoW are a good idea.
407
405
408
406
* 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.
407
+
your _views.py_. It can be a good way of thinking about your use cases separately from your web endpoints.
411
408
412
409
* You can still theoretically do DDD and domain modeling with Django models,
413
410
tightly coupled as they are to the database; you may be slowed by
@@ -422,23 +419,21 @@ https://oreil.ly/Nbpjj[word
422
419
in the Django community] is that people find that the fat models approach runs into
423
420
scalability problems of its own, particularly around managing interdependencies
424
421
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
422
+
business logic or domain layer to sit between your views and forms and
426
423
your _models.py_, which you can then keep as minimal as possible.
427
424
428
425
=== Steps Along the Way
429
426
430
427
Suppose you're working on a Django project that you're not sure is going
431
428
to get complex enough to warrant the patterns we recommend, but you still
432
429
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.((("Django", "applying patterns to Django app", "steps along the way"))) Consider the following:
430
+
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
431
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
432
+
* 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
433
forms, views, and models free of business logic. It can become a stepping-stone
438
434
for moving to a fully decoupled domain model and/or service layer later.
439
435
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
436
+
* 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
437
plain Python data structures.
443
438
444
439
* For the read side, you can get some of the benefits of CQRS by putting reads
@@ -449,10 +444,10 @@ term, and if you want to migrate to some of our patterns later.((("Django", "app
449
444
concerns will cut across them.
450
445
451
446
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
447
+
NOTE: We'd like to give a shout-out to David Seddon and Ashia Zawaduk for
448
+
talking through some of the ideas in this appendix. They did their best to
454
449
stop us from saying anything really stupid about a topic we don't really
455
450
have enough personal experience of, but they may have failed.
456
451
457
452
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>>.
453
+
applications, refer to the <<epilogue_1_how_to_get_there_from_here, epilogue>>.
Copy file name to clipboardExpand all lines: appendix_ds1_table.asciidoc
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -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,7 +50,7 @@ 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
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")))
@@ -69,7 +69,7 @@ The basic folder structure looks like this:
69
69
====
70
70
71
71
<1> Our _docker-compose.yml_ and our _Dockerfile_ are the main bits of configuration
72
-
for the containers that run our app, and can also run the tests (for CI). A
72
+
for the containers that run our app, and they can also run the tests (for CI). A
73
73
more complex project might have several Dockerfiles, although we've found that
74
74
minimizing the number of images is usually a good idea.footnote:[Splitting
75
75
out images for production and testing is sometimes a good idea, but we've tended
@@ -85,20 +85,20 @@ The basic folder structure looks like this:
85
85
team knows Python (or at least knows it better than Bash!).] This is optional. You could just use
86
86
`docker-compose` and `pytest` directly, but if nothing else, it's nice to
87
87
have all the "common commands" in a list somewhere, and unlike
88
-
documentation, a Makefile is code so it has less tendency to become out-of-date.
88
+
documentation, a Makefile is code so it has less tendency to become out of date.
89
89
90
90
<3> All the source code for our app, including the domain model, the
91
91
Flask app, and infrastructure code, lives in a Python package inside
92
92
_src_,footnote:[https://hynek.me/articles/testing-packaging["Testing and Packaging"] by Hynek Schlawack provides more information on _src_ folders.]
93
93
which we install using `pip install -e` and the _setup.py_ file. This makes
94
94
imports easy. Currently, the structure within this module is totally flat,
95
95
but for a more complex project, you'd expect to grow a folder hierarchy
96
-
including _domain_model/_, _infrastructure/_, _services/_, and _api/_.
96
+
that includes _domain_model/_, _infrastructure/_, _services/_, and _api/_.
97
97
98
98
99
99
<4> Tests live in their own folder. Subfolders distinguish different test
100
100
types and allow you to run them separately. We can keep shared fixtures
101
-
(_conftest.py_) in the main tests folder, and nest more specific ones if we
101
+
(_conftest.py_) in the main tests folder and nest more specific ones if we
102
102
wish. This is also the place to keep _pytest.ini_.
103
103
104
104
@@ -120,7 +120,7 @@ config settings for the following:
120
120
121
121
- Running on the containers themselves, with "real" ports and hostnames
122
122
123
-
- Different container environments (dev, staging, prod, and so on).
123
+
- Different container environments (dev, staging, prod, and so on)
124
124
125
125
Configuration through environment variables as suggested by the
126
126
https://12factor.net/config[12-factor] manifesto will solve this problem,
@@ -169,17 +169,16 @@ An elegant Python package called
169
169
https://github.com/hynek/environ-config[_environ-config_] is worth looking
170
170
at if you get tired of hand-rolling your own environment-based config functions.
171
171
172
-
TIP: Don't let this config module become a dumping ground full of things that
173
-
are only vaguely related to config, and is then imported all over the place.
172
+
TIP: Don't let this config module become a dumping ground that is full of things only vaguely related to config and that is then imported all over the place.
174
173
Keep things immutable and modify them only via environment variables.
175
174
If you decide to use a <<chapter_13_dependency_injection,bootstrap script>>,
176
-
you can make it the only place (other than tests) that config is imported.
175
+
you can make it the only place (other than tests) that config is imported to.
177
176
178
177
=== Docker-Compose and Containers Config
179
178
180
179
We use a lightweight Docker container orchestration tool called _docker-compose_.
181
180
It's main configuration is via a YAML file (sigh):footnote:[Harry is a bit YAML-weary.
182
-
It's _everywhere_ and yet he can never remember the syntax or how it's supposed
181
+
It's _everywhere_, and yet he can never remember the syntax or how it's supposed
183
182
to indent.]
184
183
185
184
@@ -242,7 +241,7 @@ services:
242
241
local dev machine and the container, the `PYTHONDONTWRITEBYTECODE` environment variable
243
242
tells Python to not write _.pyc_ files, and that will save you from
244
243
having millions of root-owned files sprinkled all over your local filesystem,
245
-
being all annoying to delete, and causing weird Python compiler errors besides.
244
+
being all annoying to delete and causing weird Python compiler errors besides.
246
245
247
246
<6> Mounting our source and test code as `volumes` means we don't need to rebuild
248
247
our containers every time we make a code change.
@@ -300,8 +299,8 @@ setup(
300
299
That's all you need. `packages=` specifies the names of subfolders that you
301
300
want to install as top-level modules. The `name` entry is just cosmetic, but
302
301
it's required. For a package that's never actually going to hit PyPI, it'll
303
-
do fine.footnote:[For more _setup.py_ tips see
304
-
https://hynek.me/articles/testing-packaging[this article on packaging] by Hynek.]
302
+
do fine.footnote:[For more _setup.py_ tips, see
303
+
https://oreil.ly/KMWDz[this article on packaging] by Hynek.]
305
304
306
305
307
306
=== Dockerfile
@@ -344,13 +343,13 @@ CMD flask run --host=0.0.0.0 --port=80
344
343
prod dependencies; we haven't here, for simplicity)
345
344
<3> Copying and installing our source
346
345
<4> Optionally configuring a default startup command (you'll probably override
347
-
this a lot from the command-line)
346
+
this a lot from the commandline)
348
347
349
348
TIP: One thing to note is that we install things in the order of how frequently they
350
349
are likely to change. This allows us to maximize Docker build cache reuse. I
351
-
can't tell you how much pain and frustration underlies this lesson. For this,
350
+
can't tell you how much pain and frustration underlies this lesson. For this
352
351
and many more Python Dockerfile improvement tips, check out
0 commit comments