Code Monkey home page Code Monkey logo

book's Introduction

Book repo

Book Code
Book Build Status Code build status

Table of Contents

O'Reilly have generously said that we will be able to publish this book under a CC license, In the meantime, pull requests, typofixes, and more substantial feedback + suggestions are enthusiastically solicited.

Chapter
Preface
Introduction: Why do our designs go wrong?
Part 1 Intro
Chapter 1: Domain Model Build Status
Chapter 2: Repository Build Status
Chapter 3: Interlude: Abstractions
Chapter 4: Service Layer (and Flask API) Build Status
Chapter 5: TDD in High Gear and Low Gear Build Status
Chapter 6: Unit of Work Build Status
Chapter 7: Aggregates Build Status
Part 2 Intro
Chapter 8: Domain Events and a Simple Message Bus Build Status
Chapter 9: Going to Town on the MessageBus Build Status
Chapter 10: Commands Build Status
Chapter 11: External Events for Integration Build Status
Chapter 12: CQRS Build Status
Chapter 13: Dependency Injection Build Status
Epilogue: How do I get there from here?
Appendix A: Recap table
Appendix B: Project Structure Build Status
Appendix C: A major infrastructure change, made easy Build Status
Appendix D: Django Build Status
Appendix F: Validation

Below is just instructions for me and bob really.

Dependencies:

  • asciidoctor
  • Pygments (for syntax higlighting)
  • asciidoctor-diagram (to render images from the text sources in ./images)
gem install asciidoctor
python2 -m pip install --user pygments
gem install pygments.rb
gem install asciidoctor-diagram

Commands

make html  # builds local .html versions of each chapter
make test  # does a sanity-check of the code listings

book's People

Contributors

adrienbrunet avatar aseronick avatar biladew avatar birdca avatar bobthemighty avatar daniel-butler avatar fixl avatar gaiachik avatar haibin avatar hjlarry avatar hjwp avatar hutec avatar isidroas avatar josteinl avatar katherinetozer avatar luanchang avatar lurst avatar markotibold avatar mr-bo-jangles avatar nadamsoreilly avatar nrenzoni avatar owad avatar rennerocha avatar route92 avatar sdaves avatar staticdev avatar susanmade avatar valignatev avatar xtaje avatar zhangcheng avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

book's Issues

exercises for the reader (with links to GH)

link to a broken/unfinished version of the repo for that chapter, suggest ppl implement some feature for themselves...

  • chapter 1: some placeholder tests, just test function names, an empty file to import from, a makefile to run the tests
  • chapter 2: implement a repository without an ORM?
  • chapter 3: add new service, deallocate
  • chapter 4 uow: maybe implement the uows? give them the codebase without the real uow, and without the fake uow?
  • chapter 5 aggregate: give them the repo without the aggregate, ask them to build it, and the orm.

Possible improvment to chapter 3 example 3

The following construction from Chapter 03 Example 3:

    try:
        source = tempfile.mkdtemp()
        dest = tempfile.mkdtemp()
       ...
    finally:
        shutil.rmtree(source)
        shutil.rmtree(dest)

is a use-case for TemporaryDirectory context manager:

def test_when_a_file_exists_in_the_source_but_not_the_destination():
    with tempfile.TemporaryDirectory() as source,\
           tempfile.TemporaryDirectory() as dest:

        content = "I am a very useful file"
        (Path(source) / 'my-file').write_text(content)

        sync(source, dest)

        expected_path = Path(dest) /  'my-file'
        assert expected_path.exists()
        assert expected_path.read_text() == content

Another option is to use pytest's tmpdir fixture. That would reduce number of lines even more:

def test_when_a_file_exists_in_the_source_but_not_the_destination(tmpdir):
    source = tmpdir.mkdir("source")
    dest = tmpdir.mkdir("dest")
    source.join("my-file").write("I am a very useful file")
    sync(source, dest)
    dest.join("my-file").read() == "I am a very useful file"

And there is one more thing. I understand that these end-to-end test-cases are only for demonstration of the disadvantages of tightly coupled architecture, but they do not seem very realistic to me. I would use stubs for the end-to-end testing and I would have only one test-case for that, which would rather look like this:

def test_sync_end_to_end(tmpdir):
    source = tmpdir.mkdir("source")
    dest = tmpdir.mkdir("dest")
    shutil.copytree(stub_source, source)
    shutil.copytree(stab_dest, dest)
    sync(source, dest)
    assert not filecmp.dircmp(dest, stub_source).diff_files

this test still has the same problems and disadvantages of the original test-cases, like high cost of testing all business rules and inability to test them separately and in isolation. Which actually is not the use-case for end-to-end testing. I've just understood the source of my confusion. Chapter 3 started with creating "bad" end-to-end tests and replaced them with "good" unit and edge-to-edge tests at the end of the chapter by improving the architecture of sync function. So I was under impression that the message is that it is wrong to write end-to-end tests (well, maybe I'm just a bad reader). But the point, probably, is that with the initial architecture of sync it's impossible to write unit tests, and using end-to-end testing in unit-test maner is a bad idea.

SRP and UoW in Chapter 7

I might be being far too pedantic here, but does our UoW violate the single responsibility principle by the end of Chapter 7?

It's now responsible for committing changes and handling events, which seems to flag the tip earlier in the chapter about and/then watch words.

Is this just an acceptable cost of doing business?

Chapter 4 Feedback

Chapter 4 Feedback:

Great introduction to the Unit of Work pattern. Very punchy, gets points across well.

Handful of comments:


I particularly appreciated the discussion on "Explicit vs implicit commits", as it addressed a lingering question I had from Chapter 2 regarding why you were calling commit from outside the Repository class.


I had to google the nullcontext built in - very neat!


In Example 8. Reallocate service function
uow has a products attribute. I can't seem to find where this came into our model or when we created a repository class for it (I assume it points to a ProductRepository class).

sort out diagrams

  • get all the text-based sources into a known location
  • agree a convention for naming them, referring to them in the source
  • get toolchain working via makefile command, maybe git-commit hook thingie
  • test it on atlas.

Validation Appendix

  1. event/command schemas
  2. at service layer
  3. in model (business rules)
    4 (?) at other exit boundaries?

fix some duplicate xrefs

from atlas:

warn

    WARNING: Unable to locate target for XREF with @href value: #appendix_django
    WARNING: Unable to locate target for XREF with @href value: #chapter_07_bootstrap
    AHFCmd :WARNING: Error Level : 2 AHFCmd :WARNING: Error Code : 10768 (2A10) AHFCmd :WARNING: Duplicate id value: id="callout_repository_pattern_". AHFCmd :WARNING: Line 4026, Col 82, a4a5d12f7045d343cd54832d3e671bd0.html
    AHFCmd :WARNING: Error Level : 2 AHFCmd :WARNING: Error Code : 10768 (2A10) AHFCmd :WARNING: Duplicate id value: id="get_by_batchid". AHFCmd :WARNING: Line 7447, Col 45, a4a5d12f7045d343cd54832d3e671bd0.html

error

    The ID "get_by_batchid" occurs in chapter_07_external_events.asciidoc on lines: 219, 264

Chapter 3 Feedback

Chapter 3 Feedback

Another great chapter - thanks for sharing! You guys present an incredibly accessible description/justification for creating a Service layer.

My notes:

Early on in the chapter, I was asking myself:

  • What does it mean to "Show how expressing our service layer functions in terms of primitives allows the service-layer’s users (our tests and our flask API) to be decoupled from the model layer."?

  • Are services functions? classes? Both?

They were all resolved over the course of reading the chapter.


In Example 6. Unit testing with fakes at the services layer (test_services.py)
it would be nice to have a reference to FakeSession before the code block.
When I read through it the first time I spent extra time trying to parse where it was coming from.


I had to read through test_happy_path_returns_201_and_allocated_batch several times to realize that we were allocating batch2 first because it had an earlier arrival/entry date than batch1. It might help to add a reference to this business rule again here.

Alternatively, forcing the reader to confront this issue in Example 3. Test allocations are persisted (test_api.py) by reversing the dates on batch1 and batch2 in test_allocations_are_persisted might make Example 11 more concise/punchy.


Does Example 13. Rewriting a domain test at the service layer (test_services.py) refactor a specific test from chapter 2?
If so, it would be nice to have a link back to the previous test implementation for a side-side comparison.


"As we get further into the book, we’ll see how the service layer forms an API for our system that we can drive in multiple ways. Testing against this API reduces the amount of code that we need to change when we refactor our domain model. If we restricting ourselves to only testing against the service layer, we won’t have any tests that directly interact with "private" methods or attributes on our model objects, which leaves us more free to refactor them."

This is extremely cool!

use a slightly more nested folder structure

around chapter 3/4/the project structure appendix, the tree would look like this:

│   ├── allocation
│   │   ├── adapters
│   │   │     └── flask_app.py
│   │   ├── config.py
│   │   ├── domain_model.py
│   │   ├── infrastructure
│   │   │     ├── orm.py
│   │   |     └── repository.py
│   │   └── services.py

by the end, it would look like this:

│   ├── allocation
│   │   ├── bootstrap.py
│   │   ├── config.py
│   │   ├── domain_model
│   │   │   ├── commands.py
│   │   │   ├── events.py
│   │   │   └── model.py
│   │   ├── entrypoints
│   │   │   ├── flask_app.py
│   │   │   └── redis_eventconsumer.py
│   │   ├── adapters
│   │   │   ├── email.py
│   │   │   ├── orm.py
│   │   │   ├── repository.py
│   │   │   └── redis_pubsub_client.py
│   │   ├── service_layer
│   │   │   ├── exceptions.py
│   │   │   ├── messagebus.py
│   │   │   ├── handlers.py
│   │   │   └── unit_of_work.py

Ch 2 Comments

For comparison sake, Protobuf suggests handling this problem with composition. Might be a useful sidebar.

Protocol Buffers and O-O Design Protocol buffer classes are basically dumb data holders (like structs in C); they don't make good first class citizens in an object model. If you want to add richer behaviour to a generated class, the best way to do this is to wrap the generated protocol buffer class in an application-specific class. Wrapping protocol buffers is also a good idea if you don't have control over the design of the .proto file (if, say, you're reusing one from another project). In that case, you can use the wrapper class to craft an interface better suited to the unique environment of your application: hiding some data and methods, exposing convenience functions, etc. You should never add behaviour to the generated classes by inheriting from them. This will break internal mechanisms and is not good object-oriented practice anyway.

Maybe a good opportunity to reinforce some language by saying something like, "The ORM imports (or "depends on" or "knows about") the domain model, and not the other way around.

  • Misc.
    I would find having an object or class diagram (or maybe ER diagram) upfront helpful to keep the classes, objects, and tables straight in my mind.

Query/comment on chapter 05 version numbers

The chapter says that for versions numbers, whilst the "UoW and repository could do it by magic", this is a bad idea because "there’s no real way of doing it without having to assume that all products have changed".

Why would it need to assume that all products have changed? If a command is raised to allocate an order, or to add a batch, and the command completes successfully without raising an exception, then the system knows the product in question must have changed.

Maybe I'm missing something, and the comment isn't referring to the example code immediately above? But even in more general terms, if you're doing CQRS then you know that anything that has successfully had a command executed on it has changed and therefore the version number needs to change.

I've been working on DDD projects for the last 6 years, and my main take away is that noone agrees on what any of the words or concepts mean, or even remotely on what good or bad design looks like. So maybe it's just another way of doing things! But fwiw on the projects I've worked on, people haven't been manually updating version numbers in domain service code. Later in this book mentions Event Store, which handles versioning for you, but maybe you see that as a problem? Or maybe made.com doesn't use Event Store for aggregates.

Mention attrs

Since you are using data classes pretty extensively in example code, it may be worth mentioning that prior to Python 3.7 the attrs library can be used to provide almost identical functionality.

talk about roles and responsibilities and encapsulation

see #55, also some email convo here:

(BOOK) Encapsulating the behaviour by identifying roles is a powerful tool for making
our code more expressive, more testable, and easier to maintain.

(BOB) Re: roles vs responsibilities, I think I'm using role correctly here, as the named interface of an actor that owns a responsibility. I find that what people suck at is identifying a role rather than understanding responsibilities. For example, junior devs are usually quite capable of identifying that "fetching data from an API" is a distinct responsibility in a system, but don't use that as a trigger to write some "DataFetcher" that encapsulates the responsibility behind a simpler interface. Could you articulate your reason for preferring "responsibility"?

  • (ED) re: roles vs. responsibilities, I see what you mean. I was mis-interpreting the phrase "Encapsulating behavior by identifying roles" to mean "Encapsulating interactions between roles", or "Encapsulating role-collaborations.", which make less sense relative to the DDG example. The way I've always seen these terms thrown about is "a role encapsulates a set of responsibilities", and "a collaboration encapsulates a set of interactions among roles." AFAIK "Responsibility" is a specific term-of-art, whereas "behavior" is very generic and non-specific. As a specific example, you can both say " A role encapsulates behaviors" and "A collaboration encapsulates behaviors." Or I can imagine a new developer naively going off and searching for "Behavior Driven Development", then being confused, whereas "Responsibility Driven Design" would be the better topic to research. So this might just be a case of careful word-smithing. (Maybe "Identifying roles that encapsulate behaviors" or "Encapsulating responsibilities by identifying roles")

(BOB) I agree that I'm over-widening the definition of "encapsulation". I'm happy to change it if my usage is too broad to be useful. I'm trying to demonstrate that a lot of these design choices are really driven by two overarching principles: put code in thoughtfully bounded collaborators, and think carefully about the interactions between those collaborators. I'm choosing the word "encapsulation" to mean that first principle, but maybe that's unhelpful.

  • (ED) Makes sense. I think it's pretty common for people to overload "encapsulation" to mean "information hiding" or "abstraction". I have noticed an anti-pattern, where developers who don't make these distinctions, will choose encapsulation boundaries that violate information hiding/abstraction, and end up with bad coupling/cohesion. They then don't understand why they have a BBOM. "Data fetching" is a good example of this, where everything that's related to fetching data from external APIs gets tossed into a package called "fetching", whether or not those different fetchers have anything to do with each other. Maybe instead of talking about encapsulation, just explicitly talking about roles, responsibilities, and collaborations is a good option. The notion of encapsulation should come out out naturally, because once you've identified a cohesive set of behaviors that belong together, then you encapsulate them. The appendix content where you discuss "Abstraction vs coupling" could also be a good place to talk about encapsulation boundaries.

Prologue Comments

— I’m not sure that software is inherently unstable without energy. If I applied no energy, in other words did not work on it, it might become out-of-date, un-patched and decay but not become chaotic. A big-ball of mud occurs because we trade-off the modifiability of the software against another requirement such as time-to-market or performance. We might have valid reasons to make that trade-off, but we incur a modifiability debt as a result.

— You can explain DI more easily once you have introduced layers by noting that as we depend downwards, it becomes impossible to use something from a higher layer. To correct this, you need to create an interface in your layer, and have something in the higher layer implement that. The DI is when you provide the concrete dependency when calling the lower layer. Hexagonal architectures with their ‘depend inwards’ model are even clearer here, because for the port layer to do I/O it must depend on the adapter layer above it, which it can’t do, so it creates a DAO abstraction, depends on that, and has that implemented in the adapter layer.

Domain Modelling
DDD did not originate domain modelling. It itself calls out Object Design from Rebecca Whirfs-Brock and Alan McKean, which introduced Responsibility Driven Design of which DDD is a special case, dealing with the domain. That is the book with CRC cards, object stereotypes etc. But even that is too late and you need to call out Ivar Jacobson and Grady Booch. The term has been around since the mid-1980s. The big change was perhaps that Jacobson and Whirfs-Brock called out using UML to model something other than the domain. DDD’s main ‘addition’ to our knowledge was ubiquitous language and bounded context.

Ch3 Comments


https://github.com/python-leap/book/blame/master/chapter_03_service_layer.asciidoc#L192
For the id generation, fluent helpers will make the code flow read a bit easier.

def random_sku(id_=None):
    return random_ref(''.join(['s', str(id_)]))

def random_order(id_=None):
    return random_ref(''.join(['o', str(id_)]))

def random_batch(id_=None):
    return random_ref(''.join(['b', str(id_)]))

@pytest.mark.usefixtures('restart_api')
def test_something():
    sku, order = random_sku(), random_order()
    sku1, order1 = random_sku(1), random_order(1)
    # etc., etc.

https://github.com/python-leap/book/blame/master/chapter_03_service_layer.asciidoc#L320
FWIW, pytest.raises also works with a regex

with pytest.raises(InvalidSkuException, match="nonexistentsku"):
    doit()

https://github.com/python-leap/book/blame/master/chapter_03_service_layer.asciidoc#L339
Is there supposed to be an @DataClass annotation on FakeSession? Otherwise there is a class instance and object instance "committed" variable. If not, deleting the "committed = False" line will be OK.


https://github.com/python-leap/book/blame/master/chapter_03_service_layer.asciidoc#L347
Some tangential commentary, on other styles/options here. The shunt/self-shunt pattern is also handy for this scenario, where you just need some provisional implementation. (Example later.) and the "FakeSession" in most of the examples is a dummy, rather than a fake, and a spy in two others. This could be a sidebar into discussing test doubles, mocks vs stubs, and/or London/Detroit testing. For the dummies, this would work:

# MagicMock is just creating a dummy used to satisfy the API.
# We never make any assertions on it.
services.allocate(line, repo, MagicMock())

For the spies a shunt could use just use a closure (although this makes the examples more verbose):

def test_commits():
    line = model.OrderLine('o1', 'sku1', 10)
    batch = model.Batch('b1', 'sku1', 100, eta=None)
    repo = FakeRepository([batch])

    commited = False
    class FakeSession():
        def commit(self):
            committed = True

    services.allocate(line, repo, FakeSession())
    assert session.committed is True

Or the third option could be a proper mock, with an assertion along the lines of session.commit.assert_called_once.


https://github.com/python-leap/book/blame/master/chapter_03_service_layer.asciidoc#L589
Are the source code comments supposed to be "domain-layer" and "service-layer" instead of
"model-layer" and "domain-layer"? Otherwise I'm a bit confused.


https://github.com/python-leap/book/blame/master/chapter_03_service_layer.asciidoc#L944
/add_batch route looks a bit funny to me because it's not quite RESTful, and more like RPC over HTTP. Something like POST /batches would make for a much more obvious boundary between "outside" and "inside", or "addapter" and "domain" model.

That could then help drive discussion around "Clean Architecture". The REST API does not involve Inversion-Of-Control or Dependency Inversion, and would be handy for comparing/contrasting the ideas behind "Ports-And-Adapters" vs "Functional-Core/Imperative Shell."


https://github.com/python-leap/book/blame/master/chapter_03_service_layer.asciidoc#L1081
I think the diagram makes sense, but would have more impact if there were three
diagrams instead of two -- one was for the abstractions, a second for the tests, and a third for the application.

They would all have identical topologies and layouts, but different labels (e.g. FakeRepository, AbstractRepository, SqlAlchemyRepository). That diagram could then be used to re-visit the idea of roles and collaborations.

Also, the "layers" at this point feel to me like they start to make less sense. Maybe just having a graph of depdendencies will be better (like in the Seeman blog post.)


For the Flask APIs, the blocks seem like they could flow a little easier with some small changes, and are missing a tiny bit of extra error handling.

If you have a db connection error it will tank the route handler and probably return a very unfriendly Flask-default response. (This might also be another point to talk about FunctionalCore/ImperativeShell.)

For example, for allocate route, my preference would be to do something like:

@app.route("/allocate", methods=['POST'])
def allocate_endpoint():
    try:
        session = get_session() 
        repo = repository.SqlAlchemyRepository(session)
        line = model.OrderLine(
            request.json['orderid'], 
            request.json['sku'],
            request.json['qty'], 
        )
        batchid = services.allocate(line, repo, session) 
        return jsonify({'batchid': batchid}), 201 
    except (model.OutOfStock, services.InvalidSku) as e:
        return jsonify({'message': str(e)}), 400
    except WhateverExceptionGetsThrownForDbConnectionFailures as e:
        return jsonify({'message': 'Oops! We'll be back'}), 503

Use data classes to a full potential

Reading thru chapter 1 I've noticed that some classes, like OrderLine, are data classes and some are not. It may worth considering to use data classes more to be more consistent. Also having all classes as data classes will make code more declarative.

For example Batch class:

class Batch:
    def __init__(
        self, ref: str, sku: str, qty: int, eta: Optional[date]
    ):
        self.reference = ref
        self.sku = sku
        self.eta = eta
        self._purchased_quantity = qty
        self._allocations = set()  # type: Set[OrderLine]

    def __repr__(self):
        return f'<Batch {self.reference}>'

    def __eq__(self, other):
        if not isinstance(other, Batch):
            return False
        return other.reference == self.reference

    def __hash__(self):
        return hash(self.reference)

    def __gt__(self, other):
        if self.eta is None:
            return False
        if other.eta is None:
            return True
        return self.eta > other.eta

can be declared as dataclass and it'll save some lines of code:

@dataclass(unsafe_hash=True)
class Batch:
    reference: str = field(hash=True)
    sku: str = field(compare=False)
    quantity: int = field(compare=False)
    eta: datetime = field(compare=False, default=None)
    allocations: Set[Line] = field(compare=False, default_factory=set, init=False)

    def __gt__(self, other):
        if self.eta is None:
            return False
        if other.eta is None:
            return True
        return self.eta > other.eta

Chapter 10 + appendix feedback on DI

Chapter 10 talks of injecting dependencies into command handlers (email sender, redis, etc).

Appendix A talks about 2 different ways of handling dependencies in domain code. One option is functional core/imperative shell, and have push the use of dependencies out of domain modules altogether. The other is Bob-style injection of abstracted dependencies into the domain code.

I like the contrast of Bob vs Harry approaches in the appendix, but wonder if maybe it could be merged into chapter 10, and related to the code in all the previous chapters?

In my head at least, the combination of aggregates/service layers/command handlers/event handlers and all the rest means leads to a design that is inherently FC/IS. Command handlers take some dependencies, then call domain logic on aggregates which have no dependencies. The command handlers then take some imperative actions with the results (e.g. database updates), whilst also delegating other imperative actions to message bus handlers (e.g. sending email). So the patterns described in the book are in a way a guide to how to do FC/IS in business application development - you have imperative command handlers + event handlers, and functional domain logic.

This in turn has the advantages of easy testing described in appendix A, as well as making it easier to read and reason about domain logic without being waylaid by having to consider the order of database updates or where values are being cached.

explain *why* the dependency inversion principle is a good idea tho

we sort of touch on this but don't really spell it out enough

  • in chapter 2 where we use DIP to justify repository pattern
  • and in the chapter 3 sidebar "depend on abstractions"

why the DIP? high-level modules should not depend on low-level details because:

  • low level details are hard to change, and we want to change high level stuff fast
  • ... anything else?

Ch 4 Comments


https://github.com/python-leap/book/blame/master/chapter_04_uow.asciidoc#L235
This is another good opportunity to drive home the idea of roles and collaborations. UOW and Repository are collaborators.


https://github.com/python-leap/book/blame/master/chapter_04_uow.asciidoc#L238
Maybe "Only mock your immediate neighbors" is more applicable?

I think of "Don't mock what you don't own" as referring specifically to "mock verification" (e.g. assert mock_session.commit.assert_called_once()), with the reason for this advice being that you cannot change those interfaces. So the mock has no value in providing feedback to your design.


https://github.com/python-leap/book/blame/master/chapter_04_uow.asciidoc#L437
This was confusing, since the Product class doesn't get introduced until Chapter 5.

improve tests

  • check unit test pass at end of all chapters
  • check exercise branches are not dangling

What is the point of all these patterns?

Here's a thought: a Python developer walks into Waterstones and sees this nice new book by Bob and Harry. Oh, he says, that looks interesting!

After picking it up he sees all these patterns for managing complexity, but they seem to have a lot of moving parts. What are they all for he wonders? How exactly will it help me manage complexity? Should I buy this book?

Chapter 7 Feedback

At the beginning of Chapter 7 I found that I had forgotten the domain rules that made batch quantity changes trigger reallocations.
I hopped back to Chapter 1, and was able to quickly get my bearings with the updated domain specification language.
Very useful!

I'm not sure if this information is useful to you guys, but I found skimming/lightly reading the whole chapter once and then going back and tackling it in detail was very helpful for understanding.

It would be cool to see the event flow in An internal event to express de-allocation as a diagram. Perhaps even an abstracted wall of sticky-notes?

By the time we hit Example 17. Flask creates events and puts them on the messagebus. (src/allocation/flask_app.py), it would be nice to have a formal definition of the messagebus.handle function.
My guess is that it's just a simple loop mapping an event type to a handler using the HANDLERS dictionary, but explicitly stating that would be helpful.

Very much looking forward to this:

TODO add a bit on validating input (and outputs) by using event schemas. Maybe a whole chapter on validation, including that bit about validating at the edges and not programming defensively in your inner layers.

A few longer-term things I've been thinking about while reading:

While it's been alluded to several times, I still find myself wondering how mocks play out as code smell. If we're working with external services, how do we avoid them?
I think this ties closely with my previous question regarding how we keep our Fake* objects' functionality up to date and aligned with their real counterparts.

I mostly work in django. I'm curious how much we would need to build on top of the out of the box tools to create a similar architecture, and whether or not this would feel like it was going against the grain of the framework.
I may take a stab at it in the coming weeks. If I do, I'll let you guys know.

Chapter 2 Feedback

First and foremost: awesome chapter and thanks again for writing this. I've gone through it several times now and find both the content and the writing a pleasure to read.

I found the description of the Repository design paradigm along with the intentional separation of the data store from the design model particularly cool.

Some additional thoughts/points of confusion:

In the "Is this Ports and Adapters?" sidebar, I was a little unclear how the aside fit into the broader context of the chapter.
A quick google on the definition of ports/adapters resolved it though.
Similar to some previous feedback, this is just because I'm relatively junior, and I anticipate anyone else reading it would not experience any confusion.

In the Repository test for saving an object, it is not immediately clear to me why the transaction is committed outside the add method.
Is there an expectation in flask that all database changes for a given request/response cycle happen in a single transaction and are committed at the end? It feels like it goes against our idea of an ideal object repository

In Explicit ORM Mapping with SQLALchemy Table objects, the language "The ORM imports the domain model" was not immediately intuitive to me.
It's clear from your example that you're separating the object and design model definition from ORM-specific syntax, but I had to re-read that sentence a few times before it felt like it "flowed".

Build everything using Docker

Hello, I really like your book so far 😃

I wrote the following commands to build html- and pdf-versions of the source.
Maybe some instructions like that would be helpful for some other readers, too?

test -d book && (cd book; git pull) || git clone https://github.com/python-leap/book.git
docker run -it -v $(readlink -f book):/book fedora  bash -c "yum install -y make python2 ruby; \
  gem install asciidoctor; \
  gem install --pre asciidoctor-pdf; \
  gem install pygments.rb; \
  gem install asciidoctor-diagram; \
  python2 -m pip install --user pygments; \
  cd /book; make html; \
  asciidoctor-pdf -a source-highlighter=pygments -a '!example-caption' *.asciidoc;
"

Choose a good title

throw your suggestions in here folks! Here's our current two front-runners in the "fun" and "boring" slots:

Cosmic Python

Managing Complexity using Architectural Patterns, from DDD to Ports & Adapters

(because cosmos is the opposite of chaos. don't take my word for it, trust carl sagan. don't worry, we'll explain it in the blurb...)


Managing Application Complexity with Python

Classic Enterprise Architecture Patterns, from DDD to Ports & Adapters, made Pythonic


Chapter 5 Feedback

"...also discuss similarity with eventsourcing version numbers."
Very interested in hearing more about this.

In Example 5. An integration test for concurrency behaviour (tests/integration/test_uow.py)
it might be helpful to use order1 and order2 instead of o1 and o2.

This might have been morning-brain, but I had to read the code over a few times to figure out why product version was 4 instead of 1 or 2.
Perhaps instead something like:

product_version = 3
insert_batch(session, batch, sku, 100, eta=None, product_version=product_version)
...
assert version == 4
...

Or if you're ok leaving the constant behind:

...
assert version == product_version +1
...

Table of Contents in Readme.md

Table of Contents in Readme.md still makes reference to Prologue instead of Introduction.

- | [Prologue: Why do our designs go wrong?](prologue.asciidoc)| |
+ | [Introduction: Why do our designs go wrong?](introduction.asciidoc)| |

Django guide posts

LEt's add some sidebars for Django developers to explain how these concepts map back to the framework they know and ... know

Appendix Project Structure Comments (ej)


https://github.com/python-leap/book/blame/master/appendix_project_structure.asciidoc#L73

Just dropping this pytest layout suggestions link for reference.


https://github.com/python-leap/book/blame/master/appendix_project_structure.asciidoc#L57

We have separate images for tests, using multistage builds and named stages
and generally it has been straightforward. Prior to multistage it was rather
complicated and we did not bother.


https://github.com/python-leap/book/blame/master/appendix_project_structure.asciidoc#L114

Would you consider this a singleton?

I have some past negative experiences with this style of configuration, because it can be easily abused. The env var mitigates against that, and I suppose this varies from codebase to codebase.

Edge to Edge Testing & Dependency Injection

Great seeing how things have evolved.

Since re-reading the chapter on Abstractions, I've been interested in your thoughts on testing interfaces that allow for DI and fakes vs those that don't. For example, without patching I'm not sure how one would inject a fake into a test calling a REST API.

The list of 6 items at the end of this section in Chapter 12 is helpful in this respect.

I do have one question though. Working through an example using that list, imagine that the example directory sync code from Chapter 3 is called from a CLI.

Would you have a full blown integration test calling from the CLI that does real file IO and then use the fake in the file IO code system's unittests?

A slightly related question about your list of 6 items:

At my current work place we use docker-compose to test our code and run our local environments.

However, our dev/production deployments derive from different configuration files than our local/test environments. In this case dev/prod runs off Kubernetes manifests vs the local and test docker-compose files.

From what I've heard, the old advice is to try to keep your local and test environments as close as possible to production.
Do you have advice for maintaining parity between your test/local configurations and your deployments? This question might be fairly platform specific, so no worries if it's out of band.

Chapter 6 Feedback

Very interested in this side-note: "If you find yourself using unittest.mock to test external dependencies at the service layer, it may be a code smell. See [chapter_08_bootstrap]."

This example/distinction was a really nice illustration of smell-checks for Single Responsibility Principle:
"Really this is a violation of the Single Responsibility Principle. Our use case is allocation, our endpoint, service function and domain methods are all called allocate, not allocate_and_send_mail_if_out_of_stock"

In Example 9. The UoW meets the Message Bus (src/allocation/unit_of_work.py), I got stuck trying to figure out where the seen attribute had come from.
It might be helpful to add a line explicitly introducing it before Example 9.
Once I read on to Example 10 everything cleared up immediately.

In Example 12 we have to go back and update our FakeRepository object.
In a large project with many contributors, it feels to me that keeping these fakes in sync with the real objects might become an issue.
Do you guys have any strategies for dealing with that?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.