Skip to content

Commit

Permalink
Merge remote-tracking branch 'atlas/master' into indexing
Browse files Browse the repository at this point in the history
  • Loading branch information
hjwp committed Mar 5, 2020
2 parents d42ca0c + ade928a commit ff146a9
Show file tree
Hide file tree
Showing 36 changed files with 147 additions and 175 deletions.
2 changes: 1 addition & 1 deletion appendix_django.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def test_repository_can_retrieve_a_batch_with_allocations():
d_b1 = django_models.Batch.objects.create(
reference="batch1", sku=sku, qty=100, eta=None
)
d_b2 = django_models.Batch.objects.create(
d_b2 = django_models.Batch.objects.create(
reference="batch2", sku=sku, qty=100, eta=None
)
django_models.Allocation.objects.create(line=d_line, batch=d_batch1)
Expand Down
2 changes: 1 addition & 1 deletion appendix_project_structure.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ config settings for the following:
- Different container environments (dev, staging, prod, and so on)

Configuration through environment variables as suggested by the
https://12factor.net/config[12-factor] manifesto will solve this problem,
https://12factor.net/config[12-factor manifesto] will solve this problem,
but concretely, how do we implement it in our code and our containers?


Expand Down
2 changes: 1 addition & 1 deletion appendix_validation.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ TIP: Validate as little as possible. Read only the fields you need, and don't
systems change over time. Resist the temptation to share message
definitions between systems: instead, make it easy to define the data you
depend on. For more info, see Martin Fowler's article on the
https://martinfowler.com/bliki/TolerantReader.html[Tolerant Reader pattern].
https://oreil.ly/YL_La[Tolerant Reader pattern].

[role="pagebreak-before less_space"]
.Is Postel Always Right?
Expand Down
68 changes: 35 additions & 33 deletions chapter_01_domain_model.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ But there's a lot more to DDD and to the processes, tools, and techniques for
developing a domain model. We hope to give you a taste of it, though,
and cannot encourage you enough to go on and read a proper DDD book:
* The original "blue book,"_Domain-Driven Design_ by Eric Evans (Addison-Wesley Professional)
* The original "blue book," _Domain-Driven Design_ by Eric Evans (Addison-Wesley Professional)
* The "red book," _Implementing Domain-Driven Design_
by Vaughn Vernon (Addison-Wesley Professional)
Expand Down Expand Up @@ -179,7 +179,6 @@ so that the examples are easier to talk about.
pass:[<a data-type="xref" href="#allocation_notes" data-xrefstyle="select:nopage">#allocation_notes</a>] shows some notes we might have taken while having a
conversation with our domain experts about allocation.

[role="small"]
[[allocation_notes]]
.Some Notes on Allocation
****
Expand Down Expand Up @@ -254,6 +253,7 @@ system, and the names of the classes and variables that we use are taken from th
business jargon. We could show this code to our nontechnical coworkers, and
they would agree that this correctly describes the behavior of the system.

[role="pagebreak-before"]
And here is a domain model that meets our requirements:

[[domain_model_1]]
Expand Down Expand Up @@ -368,7 +368,7 @@ So far, we can manage the implementation by just incrementing and decrementing
`Batch.available_quantity`, but as we get into `deallocate()` tests, we'll be
forced into a more intelligent solution:


[role="pagebreak-before"]
[[test_deallocate_unallocated]]
.This test is going to require a smarter model (test_batches.py)
====
Expand Down Expand Up @@ -913,6 +913,38 @@ def test_raises_out_of_stock_exception_if_cannot_allocate():
----
====


[role="nobreakinside"]
.Domain Modeling Recap
*****************************************************************
Domain modeling::
This is the part of your code that is closest to the business,
the most likely to change, and the place where you deliver the
most value to the business. Make it easy to understand and modify.
Distinguish entities from value objects::
A value object is defined by its attributes.((("value objects", "entities versus")))((("entities", "value objects versus"))) It's usually best
implemented as an immutable type. If you change an attribute on
a Value Object, it represents a different object. In contrast,
an entity has attributes that may vary over time and it will still be the
same entity. It's important to define what _does_ uniquely identify
an entity (usually some sort of name or reference field).
Not everything has to be an object::
Python is a multiparadigm language, so let the "verbs" in your
code be functions. For every `FooManager`, `BarBuilder`, or `BazFactory`,
there's often a more expressive and readable `manage_foo()`, `build_bar()`,
or `get_baz()` waiting to happen.((("functions")))
This is the time to apply your best OO design principles::
Revisit the ((("object-oriented design principles")))SOLID principles and all the other good heuristics like "has a versus is-a,"
"prefer composition over inheritance," and so on.
You'll((("domain modeling", startref="ix_dommod"))) also want to think about consistency boundaries and aggregates::
But that's a topic for <<chapter_07_aggregate>>.
*****************************************************************

We won't bore you too much with the implementation, but the main thing
to note is that we take care in naming our exceptions in the ubiquitous
language, just as we do our entities, value objects, and services:
Expand Down Expand Up @@ -946,33 +978,3 @@ image::images/apwp_0104.png[]
That'll probably do for now! We have a domain service that we can use for our
first use case.((("domain modeling", "functions for domain services", startref="ix_dommodfnc"))) But first we'll need a database...

[role="nobreakinside"]
.Domain Modeling Recap
*****************************************************************
Domain modeling::
This is the part of your code that is closest to the business,
the most likely to change, and the place where you deliver the
most value to the business. Make it easy to understand and modify.
Distinguish entities from value objects::
A value object is defined by its attributes.((("value objects", "entities versus")))((("entities", "value objects versus"))) It's usually best
implemented as an immutable type. If you change an attribute on
a Value Object, it represents a different object. In contrast,
an entity has attributes that may vary over time and it will still be the
same entity. It's important to define what _does_ uniquely identify
an entity (usually some sort of name or reference field).
Not everything has to be an object::
Python is a multiparadigm language, so let the "verbs" in your
code be functions. For every `FooManager`, `BarBuilder`, or `BazFactory`,
there's often a more expressive and readable `manage_foo()`, `build_bar()`,
or `get_baz()` waiting to happen.((("functions")))
This is the time to apply your best OO design principles::
Revisit the ((("object-oriented design principles")))SOLID principles and all the other good heuristics like "has a versus is-a,"
"prefer composition over inheritance," and so on.
You'll((("domain modeling", startref="ix_dommod"))) also want to think about consistency boundaries and aggregates::
But that's a topic for <<chapter_07_aggregate>>.
*****************************************************************
6 changes: 4 additions & 2 deletions chapter_02_repository.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ class AbstractRepository(abc.ABC):
parent class.footnote:[To really reap the benefits of ABCs (such as they
may be), be running helpers like `pylint` and `mypy`.]

<2> `raise NotImplementedError` is nice, but it's neither necessary nor sufficient. Your abstract methods can have real behavior that subclasses
<2> `raise NotImplementedError` is nice, but it's neither necessary nor sufficient. In fact, your abstract methods can have real behavior that subclasses
can call out to, if you really want.

[role="pagebreak-before less_space"]
Expand Down Expand Up @@ -838,7 +838,9 @@ summarize the costs and benefits((("Repository pattern", "and persistence ignora
We want to be clear that we're not saying every single application needs
to be built this way; only sometimes does the complexity of the app and domain
make it worth investing the time and effort in adding these extra layers of
indirection. With that in mind, <<chapter_02_repository_tradeoffs>> shows
indirection.

With that in mind, <<chapter_02_repository_tradeoffs>> shows
some of the pros and cons of the Repository pattern and our persistence-ignorant
model.

Expand Down
4 changes: 2 additions & 2 deletions chapter_03_abstractions.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ We can reduce the degree of coupling within((("coupling", "reducing by abstracti
(<<coupling_illustration1>>) by abstracting away the details
(<<coupling_illustration2>>).

[role="width-40"]
[role="width-50"]
[[coupling_illustration1]]
.Lots of coupling
image::images/apwp_0301.png[]
Expand All @@ -60,7 +60,7 @@ image::images/apwp_0301.png[]
+--------+ +--------+
----

[role="width-75"]
[role="width-90"]
[[coupling_illustration2]]
.Less coupling
image::images/apwp_0302.png[]
Expand Down
22 changes: 10 additions & 12 deletions chapter_04_service_layer.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,15 @@ add a Flask API that will talk to the service layer, which will serve as the
entrypoint to our domain model. Because our service layer depends on the
`AbstractRepository`, we can unit test it by using `FakeRepository` but run our production code using `SqlAlchemyRepository`.

NOTE: In our diagrams, we are using the convention that new components
are highlighted with bold text/lines (and yellow/orange color, if you're
reading a digital version).

[[maps_service_layer_after]]
.The service layer will become the main way into our app
image::images/apwp_0402.png[]

// IDEA more detailed legend

In our diagrams, we are using the convention that new components
are highlighted with bold text/lines (and yellow/orange color, if you're
reading a digital version).

[TIP]
====
Expand Down Expand Up @@ -721,6 +720,13 @@ Adding the service layer((("service layer", "benefits of")))((("Flask framework"
dependencies of our service ((("Flask framework", "Flask API and service layer", "service layer dependencies")))((("service layer", "dependencies of")))((("dependencies", "abstract dependencies of service layer")))layer: the domain model
and `AbstractRepository` (the port, in ports and adapters terminology).

When we run the tests, <<service_layer_diagram_test_dependencies>> shows
how we implement the abstract dependencies by using `FakeRepository` (the
adapter).((("service layer", "dependencies of", "testing")))((("dependencies", "abstract dependencies of service layer", "testing")))

And when we actually run our app, we swap in the "real" dependency((("service layer", "dependencies of", "real dependencies at runtime")))((("dependencies", "real service layer dependencies at runtime"))) shown in
<<service_layer_diagram_runtime_dependencies>>.

[role="width-75"]
[[service_layer_diagram_abstract_dependencies]]
.Abstract dependencies of the service layer
Expand All @@ -741,10 +747,6 @@ image::images/apwp_0403.png[]
----


When we run the tests, <<service_layer_diagram_test_dependencies>> shows
how we implement the abstract dependencies by using `FakeRepository` (the
adapter).((("service layer", "dependencies of", "testing")))((("dependencies", "abstract dependencies of service layer", "testing")))

[role="width-75"]
[[service_layer_diagram_test_dependencies]]
.Tests provide an implementation of the abstract dependency
Expand Down Expand Up @@ -774,9 +776,6 @@ image::images/apwp_0404.png[]
+----------------------+
----

And when we actually run our app, we swap in the "real" dependency((("service layer", "dependencies of", "real dependencies at runtime")))((("dependencies", "real service layer dependencies at runtime"))) shown in
<<service_layer_diagram_runtime_dependencies>>.

[role="width-75"]
[[service_layer_diagram_runtime_dependencies]]
.Dependencies at runtime
Expand Down Expand Up @@ -857,7 +856,6 @@ a|
controllers").((("Flask framework", "Flask API and service layer", startref="ix_Flskapp")))((("service layer", startref="ix_serlay")))
|===

[role="pagebreak-before less_space"]
But there are still some bits of awkwardness to tidy up:

* The service layer is still tightly coupled to the domain, because
Expand Down
25 changes: 13 additions & 12 deletions chapter_05_high_gear_low_gear.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ understand how the system works and how the core concepts interrelate.
We often "sketch" new behaviors by writing tests at this level to see how the
code might look. When we want to improve the design of the code, though, we will need to replace
or delete these tests, because they are tightly coupled to a particular
implementation.
pass:[<span class="keep-together">implementation</span>].

// IDEA: (EJ3) an example that is overmocked would be good here if you decide to
// add one. Ch12 already has one that could be expanded.
Expand Down Expand Up @@ -334,7 +334,7 @@ TIP: In general, if you find yourself needing to do domain-layer stuff directly
in your service-layer tests, it may be an indication that your service
layer is incomplete.


[role="pagebreak-before"]
And the implementation is just two lines:

[[add_batch_service]]
Expand Down Expand Up @@ -392,7 +392,7 @@ This is a really nice place to be in. Our service-layer tests depend on only
the service layer itself, leaving us completely free to refactor the model as
we see fit.((("test-driven development (TDD)", "fully decoupling service layer from the domain", startref="ix_TDDdecser")))((("domain layer", "fully decoupling service layer from", startref="ix_domlaydec")))((("service layer", "fully decoupling from the domain", startref="ix_serlaydec")))


[role="pagebreak-before less_space"]
=== Carrying the Improvement Through to the E2E Tests

In the same way that adding `add_batch` helped decouple our service-layer
Expand Down Expand Up @@ -470,15 +470,7 @@ def test_happy_path_returns_201_and_allocated_batch():
=== Wrap-Up

Once you have a service layer in place, you really can move the majority
of your test coverage to unit tests and develop a healthy test pyramid.((("test-driven development (TDD)", "benefits of service layer to")))((("service layer", "benefits to test-driven development"))) A few
things will help along the way:

* Express your service layer in terms of primitives rather than domain objects.

* In an ideal world, you'll have all the services you need to be able to test
entirely against the service layer, rather than hacking state via
repositories or the database.((("test-driven development (TDD)", "types of tests, rules of thumb for"))) This pays off in your end-to-end tests as well.

of your test coverage to unit tests and develop a healthy test pyramid.((("test-driven development (TDD)", "benefits of service layer to")))((("service layer", "benefits to test-driven development")))

[role="nobreakinside less_space"]
[[types_of_test_rules_of_thumb]]
Expand Down Expand Up @@ -515,4 +507,13 @@ Error handling counts as a feature::
******************************************************************************

A few
things will help along the way:

* Express your service layer in terms of primitives rather than domain objects.

* In an ideal world, you'll have all the services you need to be able to test
entirely against the service layer, rather than hacking state via
repositories or the database.((("test-driven development (TDD)", "types of tests, rules of thumb for"))) This pays off in your end-to-end tests as well.

Onto the next chapter!
14 changes: 8 additions & 6 deletions chapter_06_uow.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ layer to start a session, it talks to the repository layer to initialize
[TIP]
====
The code for this chapter is in the
chapter_06_uow branch https://oreil.ly/MoWdZ[on GitHub]:
chapter_06_uow branch https://oreil.ly/MoWdZ[on pass:[<span class="keep-together">GitHub</span>]]:
----
git clone https://github.com/cosmicpython/code.git
Expand All @@ -27,6 +27,7 @@ git checkout chapter_04_service_layer
----
====

[role="width-75"]
[[before_uow_diagram]]
.Without UoW: API talks directly to three layers
image::images/apwp_0601.png[]
Expand All @@ -39,6 +40,7 @@ to talk directly to the database.((("Unit of Work pattern", "managing database s

And we'll do it all using a lovely piece((("context manager"))) of Python syntax, a context manager.

[role="width-75"]
[[after_uow_diagram]]
.With UoW: UoW now manages database state
image::images/apwp_0602.png[]
Expand Down Expand Up @@ -309,7 +311,7 @@ def test_allocate_returns_allocation():

<2> Notice the similarity with the fake `commit()` function
from `FakeSession` (which we can now get rid of). But it's
a substantial improvement because we're now faking out
a substantial improvement because we're now pass:[<span class="keep-together">faking</span>] out
code that we wrote rather than third-party code. Some
people say, https://oreil.ly/0LVj3["Don't mock what you don't own"].

Expand Down Expand Up @@ -578,10 +580,6 @@ it's doing are covered in _test_repository.py_. That last test, you might keep a
but we could certainly see an argument for just keeping everything at the highest
possible level of abstraction (just as we did for the unit tests).

TIP: This is another example of the lesson from <<chapter_05_high_gear_low_gear>>:
as we build better abstractions, we can move our tests to run against them,
which leaves us free to change the underlying details.

[role="nobreakinside less_space"]
.Exercise for the Reader
******************************************************************************
Expand All @@ -599,6 +597,10 @@ the abstract UoW. Why not send us a link to your repo if you come up with
something you're particularly proud of?
******************************************************************************

TIP: This is another example of the lesson from <<chapter_05_high_gear_low_gear>>:
as we build better abstractions, we can move our tests to run against them,
which leaves us free to change the underlying details.


=== Wrap-Up

Expand Down
Loading

0 comments on commit ff146a9

Please sign in to comment.