Skip to content

Commit

Permalink
Merge branch 'indexing-experiment-realbook'
Browse files Browse the repository at this point in the history
  • Loading branch information
hjwp committed Mar 20, 2020
2 parents 6a468d7 + 41e62de commit 45b904a
Show file tree
Hide file tree
Showing 18 changed files with 1,468 additions and 626 deletions.
12 changes: 8 additions & 4 deletions appendix_csvs.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
[appendix]
== Swapping Out the Infrastructure: [.keep-together]#Do Everything with CSVs#

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

Just as we finish building out our Flask API and getting it ready for release,
Expand Down Expand Up @@ -178,7 +179,8 @@ with CSVs underlying them instead of a database. And as you'll see, it really is
=== Implementing a Repository and Unit of Work for CSVs


Here's what a CSV-based repository could look like.((("repositories", "CSV-based repository"))) It abstracts away all the
((("repositories", "CSV-based repository")))
Here's what a CSV-based repository could look like. It abstracts away all the
logic for reading CSVs from disk, including the fact that it has to read _two
different CSVs_ (one for batches and one for allocations), and it gives us just
the familiar `.list()` API, which provides the illusion of an in-memory
Expand Down Expand Up @@ -235,7 +237,8 @@ class CsvRepository(repository.AbstractRepository):
// TODO (hynek) re self._load(): DUDE! no i/o in init!


And here's((("Unit of Work pattern", "UoW for CSVs"))) what a UoW for CSVs would look like:
((("Unit of Work pattern", "UoW for CSVs")))
And here's what a UoW for CSVs would look like:



Expand Down Expand Up @@ -289,7 +292,8 @@ def main(folder):
====


Ta-da! _Now are y'all impressed or what_?((("CSVs, doing everything with", startref="ix_CSV")))
((("CSVs, doing everything with", startref="ix_CSV")))
Ta-da! _Now are y'all impressed or what_?

Much love,

Expand Down
46 changes: 34 additions & 12 deletions appendix_django.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
[appendix]
== Repository and Unit of Work [.keep-together]#Patterns with Django#

((("Django", "installing")))
((("Django", id="ix_Django")))
Suppose you wanted to use Django instead of SQLAlchemy and Flask. How
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
might things look? The first thing is to choose where to install it. We put it in a separate
package next to our main allocation code:


Expand Down Expand Up @@ -60,9 +62,12 @@ git checkout appendix_django

=== Repository Pattern with Django

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

Rewriting the first repository test was a minimal change—just rewriting
some raw SQL with a call to the Django ORM/QuerySet language:
Expand Down Expand Up @@ -162,7 +167,9 @@ help minimize boilerplate for this sort of thing.]

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

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:
((("domain model", "Django custom ORM methods for conversion")))
((("object-relational mappers (ORMs)", "Django, custom methods to translate to/from domain model")))
Those custom methods look something like this:

[[django_models]]
.Django ORM with custom methods for domain model conversion (src/djangoproject/alloc/models.py)
Expand Down Expand Up @@ -222,14 +229,18 @@ class OrderLine(models.Model):


NOTE: As in <<chapter_02_repository>>, we use dependency inversion.
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")))
The ORM (Django) depends on the model and not the other way around.
((("Django", "Repository pattern with", startref="ix_DjangoRepo")))
((("Repository pattern", "with Django", startref="ix_RepoDjango")))



=== Unit of Work Pattern with Django


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:
((("Django", "Unit of Work pattern with", id="ix_DjangoUoW")))
((("Unit of Work pattern", "with Django", id="ix_UoWDjango")))
The tests don't change too much:

[[test_uow_django]]
.Adapted UoW tests (tests/integration/test_uow.py)
Expand Down Expand Up @@ -320,13 +331,19 @@ class DjangoUnitOfWork(AbstractUnitOfWork):
instrumenting the domain model instances themselves, the
`commit()` command needs to explicitly go through all the
objects that have been touched by every repository and manually
update them back to the ORM.((("Unit of Work pattern", "with Django", startref="ix_UoWDjango")))((("Django", "Unit of Work pattern with", startref="ix_DjangoUoW")))
update them back to the ORM.
((("Django", "Unit of Work pattern with", startref="ix_DjangoUoW")))
((("Unit of Work pattern", "with Django", startref="ix_UoWDjango")))



=== API: Django Views Are Adapters

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
((("adapters", "Django views")))
((("views", "Django views as adapters")))
((("APIs", "Django views as adapters")))
((("Django", "views are adapters")))
The Django _views.py_ file ends up being almost identical to the
old _flask_app.py_, because our architecture means it's a very
thin wrapper around our service layer (which didn't change at all, by the way):

Expand Down Expand Up @@ -371,8 +388,9 @@ def allocate(request):

=== Why Was This All So Hard?

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

The main reason at a low level is because Django's ORM doesn't work in the same
way. We don't have an equivalent of the SQLAlchemy classical mapper, so our
Expand All @@ -381,10 +399,11 @@ build a manual translation layer behind the repository. That's more
work (although once it's done, the ongoing maintenance burden shouldn't be too
high).

((("pytest", "pytest-django plugin")))
Because Django is so tightly coupled to the database, you have to use helpers
like `pytest-django` and think carefully about test databases, right from
the very first line of code, in a way that we didn't have to when we started
out with our pure domain model.((("pytest", "pytest-django plugin")))
out with our pure domain model.

But at a higher level, the entire reason that Django is so great
is that it's designed around the sweet spot of making it easy to build CRUD
Expand All @@ -398,8 +417,9 @@ around the workflow of state changes. The Django admin bypasses all of that.

=== What to Do If You Already Have Django

((("Django", "applying patterns to Django app")))
So what should you do if you want to apply some of the patterns in this book
to a Django app?((("Django", "applying patterns to Django app"))) We'd say the following:
to a Django app? We'd say the following:

* The Repository and Unit of Work patterns are going to be quite a lot of work. The
main thing they will buy you in the short term is faster unit tests, so
Expand Down Expand Up @@ -428,10 +448,11 @@ your _models.py_, which you can then keep as minimal as possible.

=== Steps Along the Way

((("Django", "applying patterns to Django app", "steps along the way")))
Suppose you're working on a Django project that you're not sure is going
to get complex enough to warrant the patterns we recommend, but you still
want to put a few steps in place to make your life easier, both in the medium
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:
term and if you want to migrate to some of our patterns later. Consider the following:

* 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
forms, views, and models free of business logic. It can become a stepping-stone
Expand All @@ -454,5 +475,6 @@ NOTE: We'd like to give a shout-out to David Seddon and Ashia Zawaduk for
stop us from saying anything really stupid about a topic we don't really
have enough personal experience of, but they may have failed.

For more ((("Django", startref="ix_Django")))thoughts and actual lived experience dealing with existing
((("Django", startref="ix_Django")))
For more thoughts and actual lived experience dealing with existing
applications, refer to the <<epilogue_1_how_to_get_there_from_here, epilogue>>.
7 changes: 4 additions & 3 deletions appendix_ds1_table.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
[appendix]
== Summary Diagram and Table

Here's what our architecture looks((("architecture, summary diagram and table", id="ix_archsumm"))) like by the end of the book:
((("architecture, summary diagram and table", id="ix_archsumm")))
Here's what our architecture looks like by the end of the book:

[[recap_diagram]]
image::images/apwp_aa01.png["diagram showing all components: flask+eventconsumer, service layer, adapters, domain etc"]
Expand Down Expand Up @@ -53,6 +54,6 @@ __Translate external inputs into calls into the service layer.__
| Web | Receives web requests and translates them into commands, passing them to the internal message bus.
| Event consumer | Reads events from the external message bus and translates them into commands, passing them to the internal message bus.

| 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")))
| 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")))
9 changes: 6 additions & 3 deletions appendix_project_structure.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
[appendix]
== A Template Project Structure

((("projects", "template project structure", id="ix_prjstrct")))
Around <<chapter_04_service_layer>>, we moved from just having
everything in one folder to a more structured tree, and we thought it might
be of interest to outline the moving parts.((("projects", "template project structure", id="ix_prjstrct")))
be of interest to outline the moving parts.

[TIP]
====
Expand Down Expand Up @@ -353,7 +354,8 @@ TIP: One thing to note is that we install things in the order of how frequently

=== Tests

Our tests ((("testing", "tests folder tree")))are kept alongside everything else, as shown here:
((("testing", "tests folder tree")))
Our tests are kept alongside everything else, as shown here:

[[tests_folder]]
.Tests folder tree
Expand Down Expand Up @@ -394,5 +396,6 @@ These are our basic building blocks:
* Configuration via environment variables, centralized in a Python file called _config.py_, with defaults allowing things to run _outside_ containers
* A Makefile for useful command-line, um, commands

((("projects", "template project structure", startref="ix_prjstrct")))
We doubt that anyone will end up with _exactly_ the same solutions we did, but we hope you
find some inspiration here.((("projects", "template project structure", startref="ix_prjstrct")))
find some inspiration here.
6 changes: 4 additions & 2 deletions appendix_validation.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
[appendix]
== Validation

Whenever we're ((("validation", id="ix_valid")))teaching and talking about these techniques, one question that
((("validation", id="ix_valid")))
Whenever we're teaching and talking about these techniques, one question that
comes up over and over is "Where should I do validation? Does that belong with
my business logic in the domain model, or is that an infrastructural concern?"

Expand Down Expand Up @@ -508,11 +509,12 @@ Locate each of the three types of validation in the right place::
TIP: Once you've validated the syntax and semantics of your commands
at the edges of your system, the domain is the place for the rest
of your validation. Validation of pragmatics is often a core part
of your business rules.((("validation", startref="ix_valid")))
of your business rules.


In software terms, the pragmatics of an operation are usually managed by the
domain model. When we receive a message like "allocate three million units of
`SCARCE-CLOCK` to order 76543," the message is _syntactically_ valid and
_semantically_ valid, but we're unable to comply because we don't have the stock
available.
((("validation", startref="ix_valid")))
Loading

0 comments on commit 45b904a

Please sign in to comment.