Skip to content

Commit 67dfe67

Browse files
authored
Merge pull request #257 from cosmicpython/indexing
Indexing & QC2 (final edits before going to print)
2 parents 7ffacd4 + 5013d96 commit 67dfe67

File tree

81 files changed

+1744
-1700
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+1744
-1700
lines changed

appendix_csvs.asciidoc

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
== Swapping Out the Infrastructure: pass:[<span class="keep-together">Do Everything with CSVs</span>]
44

55
This appendix is intended as a little illustration of the benefits of the
6-
Repository, Unit of Work, and Service Layer patterns. It's intended to
6+
Repository, Unit of Work, and Service Layer patterns.((("CSVs, doing everything with", id="ix_CSV"))) It's intended to
77
follow from <<chapter_06_uow>>.
88

99
Just as we finish building out our Flask API and getting it ready for release,
1010
the business comes to us apologetically, saying they're not ready to use our API
11-
and could we build a thing that reads just batches and orders from a couple of
12-
CSVs and outputs a third with allocations.
11+
and asking if we could build a thing that reads just batches and orders from a couple of
12+
CSVs and outputs a third CSV with allocations.
1313

1414
Ordinarily this is the kind of thing that might have a team cursing and spitting
1515
and making notes for their memoirs. But not us! Oh no, we've ensured that
@@ -169,7 +169,7 @@ def test_cli_app_also_reads_existing_allocations_and_can_append_to_them(
169169

170170

171171
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.
173173

174174
All we need to do ("all we need to do") is reimplement those same abstractions, but
175175
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
178178
=== Implementing a Repository and Unit of Work for CSVs
179179

180180

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
182182
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
185185
collection of domain objects:
186186

187187
[[csv_repository]]
@@ -235,12 +235,12 @@ class CsvRepository(repository.AbstractRepository):
235235
// TODO (hynek) re self._load(): DUDE! no i/o in init!
236236

237237

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:
239239

240240

241241

242242
[[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)
244244
====
245245
[source,python]
246246
----
@@ -266,11 +266,11 @@ class CsvUnitOfWork(unit_of_work.AbstractUnitOfWork):
266266

267267

268268
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 bea bit
270270
of code for reading order lines, and a bit of code that invokes our
271271
_existing_ service layer:
272272

273-
273+
[role="nobreakinside less_space"]
274274
[[final_cli]]
275275
.Allocation with CSVs in nine lines (src/bin/allocate-from-csv)
276276
====
@@ -289,7 +289,7 @@ def main(folder):
289289
====
290290

291291

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")))
293293

294294
Much love,
295295

appendix_django.asciidoc

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
== Repository and Unit of Work pass:[<span class="keep-together">Patterns with Django</span>]
44

55
Suppose you wanted to use Django instead of SQLAlchemy and Flask. How
6-
might things look?
7-
8-
The first thing is to choose where to install it. We put it in a separate
6+
might things look?((("Django", id="ix_Django"))) The first thing is to choose where to install it.((("Django", "installing"))) We put it in a separate
97
package next to our main allocation code:
108

119

@@ -49,8 +47,8 @@ package next to our main allocation code:
4947

5048
[TIP]
5149
====
52-
You can find the code for this chapter is in the
53-
https://github.com/cosmicpython/code/tree/appendix_django[appendix_django] branch on GitHub.
50+
The code for this appendix is in the
51+
appendix_django branch https://oreil.ly/A-I76[on GitHub]:
5452
5553
----
5654
git clone https://github.com/cosmicpython/code.git
@@ -62,11 +60,11 @@ git checkout appendix_django
6260

6361
=== Repository Pattern with Django
6462

65-
We used a plug in called
63+
We used a plug-in called
6664
https://github.com/pytest-dev/pytest-django[`pytest-django`] to help with test
67-
database management.
65+
database management.((("pytest", "pytest-django plug-in")))((("Repository pattern", "with Django", id="ix_RepoDjango")))((("Django", "Repository pattern with", id="ix_DjangoRepo")))
6866

69-
Rewriting the first repository test was a minimal change, just rewriting
67+
Rewriting the first repository test was a minimal changejust rewriting
7068
some raw SQL with a call to the Django ORM/QuerySet language:
7169

7270

@@ -106,8 +104,12 @@ but it is still made up of familiar-looking Django code:
106104
def test_repository_can_retrieve_a_batch_with_allocations():
107105
sku = "PONY-STATUE"
108106
d_line = django_models.OrderLine.objects.create(orderid="order1", sku=sku, qty=12)
109-
d_batch1 = django_models.Batch.objects.create(reference="batch1", sku=sku, qty=100, eta=None)
110-
d_batch2 = django_models.Batch.objects.create(reference="batch2", sku=sku, qty=100, eta=None)
107+
d_b1 = django_models.Batch.objects.create(
108+
reference="batch1", sku=sku, qty=100, eta=None
109+
)
110+
d_b2 = django_models.Batch.objects.create(
111+
reference="batch2", sku=sku, qty=100, eta=None
112+
)
111113
django_models.Allocation.objects.create(line=d_line, batch=d_batch1)
112114
113115
repo = repository.DjangoRepository()
@@ -160,7 +162,7 @@ help minimize boilerplate for this sort of thing.]
160162

161163
==== Custom Methods on Django ORM Classes to Translate to/from Our Domain Model
162164

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:
164166

165167
[[django_models]]
166168
.Django ORM with custom methods for domain model conversion (src/djangoproject/alloc/models.py)
@@ -209,7 +211,7 @@ class OrderLine(models.Model):
209211

210212
<1> For value objects, `objects.get_or_create` can work, but for entities,
211213
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`],
213215
but that's beyond our Django-fu.]
214216

215217
<2> We've shown the most complex example here. If you do decide to do this,
@@ -220,14 +222,14 @@ class OrderLine(models.Model):
220222

221223

222224
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")))
224226

225227

226228

227229
=== Unit of Work Pattern with Django
228230

229231

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:
231233

232234
[[test_uow_django]]
233235
.Adapted UoW tests (tests/integration/test_uow.py)
@@ -309,7 +311,7 @@ class DjangoUnitOfWork(AbstractUnitOfWork):
309311
====
310312

311313
<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
313315
begin a transaction.
314316

315317
<2> Then we use the explicit rollback and commits.
@@ -318,13 +320,13 @@ class DjangoUnitOfWork(AbstractUnitOfWork):
318320
instrumenting the domain model instances themselves, the
319321
`commit()` command needs to explicitly go through all the
320322
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")))
322324

323325

324326

325327
=== API: Django Views Are Adapters
326328

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
328330
old _flask_app.py_, because our architecture means it's a very
329331
thin wrapper around our service layer (which didn't change at all, by the way):
330332

@@ -370,7 +372,7 @@ def allocate(request):
370372
=== Why Was This All So Hard?
371373

372374
OK, it works, but it does feel like more effort than Flask/SQLAlchemy. Why is
373-
that?
375+
that?((("Django", "using, difficulty of")))
374376

375377
The main reason at a low level is because Django's ORM doesn't work in the same
376378
way. We don't have an equivalent of the SQLAlchemy classical mapper, so our
@@ -382,7 +384,7 @@ high).
382384
Because Django is so tightly coupled to the database, you have to use helpers
383385
like `pytest-django` and think carefully about test databases, right from
384386
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")))
386388

387389
But at a higher level, the entire reason that Django is so great
388390
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.
397399
=== What to Do If You Already Have Django
398400

399401
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:
401403

402404
* The Repository and Unit of Work patterns are going to be quite a lot of work. The
403405
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
405407
decouple your app from Django and the database, so if you anticipate wanting
406408
to migrate away from either of those, Repository and UoW are a good idea.
407409

408410
* 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.
411412

412413
* You can still theoretically do DDD and domain modeling with Django models,
413414
tightly coupled as they are to the database; you may be slowed by
@@ -422,25 +423,24 @@ https://oreil.ly/Nbpjj[word
422423
in the Django community] is that people find that the fat models approach runs into
423424
scalability problems of its own, particularly around managing interdependencies
424425
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
426427
your _models.py_, which you can then keep as minimal as possible.
427428

428429
=== Steps Along the Way
429430

430431
Suppose you're working on a Django project that you're not sure is going
431432
to get complex enough to warrant the patterns we recommend, but you still
432433
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:
434435

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
437437
forms, views, and models free of business logic. It can become a stepping-stone
438438
for moving to a fully decoupled domain model and/or service layer later.
439439

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
442441
plain Python data structures.
443442

443+
[role="pagebreak-before"]
444444
* For the read side, you can get some of the benefits of CQRS by putting reads
445445
into one place, avoiding ORM calls sprinkled all over the place.
446446

@@ -449,10 +449,10 @@ term, and if you want to migrate to some of our patterns later. Consider the fol
449449
concerns will cut across them.
450450

451451

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 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
454454
stop us from saying anything really stupid about a topic we don't really
455455
have enough personal experience of, but they may have failed.
456456

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>>.

appendix_ds1_table.asciidoc

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
[appendix]
33
== Summary Diagram and Table
44

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:
66

77
[[recap_diagram]]
88
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
2020
__Defines the business logic.__
2121

2222

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.
2424

2525
| Value object | An immutable domain object whose attributes entirely define it. It is fungible with other identical objects.
2626

@@ -34,7 +34,7 @@ __Defines the business logic.__
3434

3535
__Defines the jobs the system should perform and orchestrates different components.__
3636

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.
3838
| Unit of work | Abstraction around data integrity. Each unit of work represents an atomic update. Makes repositories available. Tracks new events on retrieved aggregates.
3939
| Message bus (internal) | Handles commands and events by routing them to the appropriate handler.
4040

@@ -50,9 +50,9 @@ to the outside world (I/O).__
5050

5151
__Translate external inputs into calls into the service layer.__
5252

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.
5454
| Event consumer | Reads events from the external message bus and translates them into commands, passing them to the internal message bus.
5555

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")))
5757
|===
5858

0 commit comments

Comments
 (0)