diff --git a/appendix_validation.asciidoc b/appendix_validation.asciidoc index 2bc38a93..6fd2eb4c 100644 --- a/appendix_validation.asciidoc +++ b/appendix_validation.asciidoc @@ -63,9 +63,9 @@ from schema import And, Schema, Use class Allocate(Command): _schema = Schema({ #<1> - 'orderid': int, - sku: str, - qty: And(Use(int), lambda n: n > 0) + 'orderid': str, + 'sku': str, + 'qty': And(Use(int), lambda n: n > 0) }, ignore_extra_keys=True) orderid: str @@ -74,8 +74,8 @@ class Allocate(Command): @classmethod def from_json(cls, data): #<2> - data = json.loads(data) - return cls(**_schema.validate(data)) + data = json.loads(data) + return cls(**_schema.validate(data)) ---- ==== @@ -111,12 +111,14 @@ def greater_than_zero(x): quantity = And(Use(int), greater_than_zero) #<4> Allocate = command( #<5> + 'Allocate', orderid=int, sku=str, qty=quantity ) AddStock = command( + 'AddStock', sku=str, qty=quantity ---- @@ -298,7 +300,7 @@ def handle_change_batch_quantity(m, bus: messagebus.MessageBus): try: bus.handle_message('ChangeBatchQuantity', m) except ValidationError: - print('Skipping invalid message') + print('Skipping invalid message') except exceptions.InvalidSku as e: print(f'Unable to change stock for missing sku {e}') ---- @@ -360,10 +362,10 @@ class MessageUnprocessable(Exception): #<1> self.message = message class ProductNotFound(MessageUnprocessable): #<2> - """" - This exception is raised when we try to perform an action on a product - that doesn't exist in our database. - """" + """" + This exception is raised when we try to perform an action on a product + that doesn't exist in our database. + """" def __init__(self, message): super().__init__(message) @@ -448,9 +450,9 @@ class MessageBus: def handle_message(self, message): try: - ... - except SkipMessage as e: - logging.warn(f"Skipping message {message.id} because {e.reason}") + ... + except SkipMessage as e: + logging.warn(f"Skipping message {message.id} because {e.reason}") ---- ==== diff --git a/chapter_01_domain_model.asciidoc b/chapter_01_domain_model.asciidoc index f1df86a8..194a8526 100644 --- a/chapter_01_domain_model.asciidoc +++ b/chapter_01_domain_model.asciidoc @@ -232,7 +232,7 @@ how we would construct a model from this business conversation. ****************************************************************************** Why not have a go at solving this problem yourself? Write a few unit tests to see if you can capture the essence of these business rules in nice, clean -code. +code (ideally without looking at the solution we came up with below!) You'll find some https://github.com/cosmicpython/code/tree/chapter_01_domain_model_exercise[placeholder unit tests on GitHub], but you could just start from scratch, or combine/rewrite them however you like. @@ -287,8 +287,8 @@ class Batch: self.eta = eta self.available_quantity = qty - def allocate(self, line: OrderLine): - self.available_quantity -= line.qty #<3> + def allocate(self, line: OrderLine): #<3> + self.available_quantity -= line.qty ---- ==== diff --git a/chapter_02_repository.asciidoc b/chapter_02_repository.asciidoc index d6b2568d..cd7bd7fe 100644 --- a/chapter_02_repository.asciidoc +++ b/chapter_02_repository.asciidoc @@ -548,7 +548,8 @@ class AbstractRepository(abc.ABC): ((("@abc.abstractmethod"))) ((("abstract methods"))) -<2> `raise NotImplementedError` is nice, but it's neither necessary nor sufficient. In fact, 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"] @@ -805,7 +806,8 @@ def allocate_endpoint(): We bumped into a friend at a DDD conference the other day who said, "I haven't used an ORM in 10 years." The Repository pattern and an ORM both act as abstractions in front of raw SQL, so using one behind the other isn't really necessary. Why -not have a go at implementing our repository without using the ORM? You'll find the code https://github.com/cosmicpython/code/tree/chapter_02_repository_exercise[on GitHub]. +not have a go at implementing our repository without using the ORM? +You'll find the code https://github.com/cosmicpython/code/tree/chapter_02_repository_exercise[on GitHub]. We've left the repository tests, but figuring out what SQL to write is up to you. Perhaps it'll be harder than you think; perhaps it'll be easier. @@ -904,7 +906,7 @@ summarize the costs and benefits of each architectural pattern we introduce. 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. +indirection. With that in mind, <> shows some of the pros and cons of the Repository pattern and our persistence-ignorant diff --git a/chapter_03_abstractions.asciidoc b/chapter_03_abstractions.asciidoc index 1d9c509b..8f7af2a8 100644 --- a/chapter_03_abstractions.asciidoc +++ b/chapter_03_abstractions.asciidoc @@ -24,7 +24,8 @@ git checkout chapter_03_abstractions A key theme in this book, hidden among the fancy patterns, is that we can use simple abstractions to hide messy details. When we're writing code for fun, or in a kata,footnote:[A code kata is a small, contained programming challenge often -used to practice TDD. See https://oreil.ly/vhjju["Kata—The Only Way to Learn TDD"] by Peter Provost.] +used to practice TDD. See +https://web.archive.org/web/20221024055359/http://www.peterprovost.org/blog/2012/05/02/kata-the-only-way-to-learn-tdd/["Kata—The Only Way to Learn TDD"] by Peter Provost.] we get to play with ideas freely, hammering things out and refactoring aggressively. In a large-scale system, though, we become constrained by the decisions made elsewhere in the system. @@ -783,7 +784,7 @@ story we care about. ((("PyCon talk on Mocking Pitfalls"))) ((("Jung, Ed"))) Steve Freeman has a great example of overmocked tests in his talk -https://oreil.ly/jAmtr["Test-Driven Development"]. +https://youtu.be/yuEbZYKgZas?si=ZpBoivlDH13XTG9p&t=294["Test-Driven Development: That's Not What We Meant"]. You should also check out this PyCon talk, https://oreil.ly/s3e05["Mocking and Patching Pitfalls"], by our esteemed tech reviewer, Ed Jung, which also addresses mocking and its alternatives. diff --git a/chapter_04_service_layer.asciidoc b/chapter_04_service_layer.asciidoc index 089220af..83bf6a57 100644 --- a/chapter_04_service_layer.asciidoc +++ b/chapter_04_service_layer.asciidoc @@ -558,7 +558,7 @@ def allocate_endpoint(): <1> We instantiate a database session and some repository objects. <2> We extract the user's commands from the web request and pass them - to a domain service. + to the service layer. <3> We return some JSON responses with the appropriate status codes. The responsibilities of the Flask app are just standard web stuff: per-request diff --git a/chapter_05_high_gear_low_gear.asciidoc b/chapter_05_high_gear_low_gear.asciidoc index bfb1366e..a6d8550d 100644 --- a/chapter_05_high_gear_low_gear.asciidoc +++ b/chapter_05_high_gear_low_gear.asciidoc @@ -48,7 +48,7 @@ does to our test pyramid: [source,sh] [role="skip"] ---- -$ grep -c test_ **/test_*.py +$ grep -c test_ */*/test_*.py tests/unit/test_allocate.py:4 tests/unit/test_batches.py:8 tests/unit/test_services.py:3 @@ -310,7 +310,7 @@ function on `FakeRepository`: [source,python] [role="skip"] ---- -class FakeRepository(set): +class FakeRepository(repository.AbstractRepository): @staticmethod def for_batch(ref, sku, qty, eta=None): diff --git a/chapter_06_uow.asciidoc b/chapter_06_uow.asciidoc index 9420e0ad..24c9a2a2 100644 --- a/chapter_06_uow.asciidoc +++ b/chapter_06_uow.asciidoc @@ -169,11 +169,11 @@ def insert_batch(session, ref, sku, qty, eta): def get_allocated_batch_ref(session, orderid, sku): - [[orderlineid]] = session.execute( + [[orderlineid]] = session.execute( #<1> "SELECT id FROM order_lines WHERE orderid=:orderid AND sku=:sku", dict(orderid=orderid, sku=sku), ) - [[batchref]] = session.execute( + [[batchref]] = session.execute( #<1> "SELECT b.reference FROM allocations JOIN batches AS b ON batch_id = b.id" " WHERE orderline_id=:orderlineid", dict(orderlineid=orderlineid), @@ -182,7 +182,15 @@ def get_allocated_batch_ref(session, orderid, sku): ---- ==== -// TODO: that double-unpacking is freaking ppl out. maybe [(orderlineid, )] ? +<1> The `[[orderlineid]] =` syntax is a little too-clever-by-half, apologies. + What's happening is that `session.execute` returns a list of rows, + where each row is a tuple of column values; + in our specific case, it's a list of one row, + which is a tuple with one column value in. + The double-square-bracket on the left hand side + is doing (double) assignment-unpacking to get the single value + back out of these two nested sequences. + It becomes readable once you've used it a few times! === Unit of Work and Its Context Manager diff --git a/chapter_09_all_messagebus.asciidoc b/chapter_09_all_messagebus.asciidoc index df129145..0ef9a65d 100644 --- a/chapter_09_all_messagebus.asciidoc +++ b/chapter_09_all_messagebus.asciidoc @@ -845,10 +845,9 @@ class FakeUnitOfWorkWithFakeMessageBus(FakeUnitOfWork): super().__init__() self.events_published = [] # type: List[events.Event] - def publish_events(self): - for product in self.products.seen: - while product.events: - self.events_published.append(product.events.pop(0)) + def collect_new_events(self): + self.events_published += super().collect_new_events() + return [] ---- ==== diff --git a/code b/code index 3ed6ff0f..734df09a 160000 --- a/code +++ b/code @@ -1 +1 @@ -Subproject commit 3ed6ff0fab52e14edba6ced4b258af68c521115f +Subproject commit 734df09afc65ba43c851271def147c70ac3c3b98 diff --git a/epilogue_1_how_to_get_there_from_here.asciidoc b/epilogue_1_how_to_get_there_from_here.asciidoc index c5e91f91..aa824b21 100644 --- a/epilogue_1_how_to_get_there_from_here.asciidoc +++ b/epilogue_1_how_to_get_there_from_here.asciidoc @@ -631,7 +631,7 @@ storming and CRC modeling, because humans are good at collaborating through play. _Event modeling_ is another technique that brings engineers and product owners together to understand a system in terms of commands, queries, and events. -TIP: Check out _www.eventmodeling.org_ and _www.eventstorming.org_ for some great +TIP: Check out _www.eventmodeling.org_ and _www.eventstorming.com_ for some great guides to visual modeling of systems with events. The goal is to be able to talk about the system by using the same ubiquitous diff --git a/part1.asciidoc b/part1.asciidoc index a44eecf2..2d357b7d 100644 --- a/part1.asciidoc +++ b/part1.asciidoc @@ -32,7 +32,7 @@ To do that, we present four key design patterns: * The <>, an abstraction over the idea of persistent storage -* The <> pattern to clearly define where our +* The <> to clearly define where our use cases begin and end [role="pagebreak-before"] @@ -60,7 +60,7 @@ Three appendices are further explorations of the content from Part I: code: how we build and run the Docker images, where we manage configuration info, and how we run different types of tests. -* <> is a "proof is in the pudding" kind of content, showing +* <> is a "proof of the pudding" kind of content, showing how easy it is to swap out our entire infrastructure--the Flask API, the ORM, and Postgres—for a totally different I/O model involving a CLI and CSVs. diff --git a/preface.asciidoc b/preface.asciidoc index d132ec4d..d8b98e78 100644 --- a/preface.asciidoc +++ b/preface.asciidoc @@ -347,6 +347,7 @@ Ben Judson, James Gregory, Łukasz Lechowicz, Clinton Roy, Vitorino Araújo, Susan Goodbody, Josh Harwood, Daniel Butler, Liu Haibin, Jimmy Davies, Ignacio Vergara Kausel, Gaia Canestrani, Renne Rocha, pedroabi, Ashia Zawaduk, Jostein Leira, Brandon Rhodes, Jazeps Basko, simkimsia, Adrien Brunet, Sergey Nosko, +Dmitry Bychkov, dayres2, programmer-ke, asjhita, Filip Lajszczak, and many more; our apologies if we missed you on this list. Super-mega-thanks to our editor Corbin Collins for his gentle chivvying, and @@ -355,6 +356,4 @@ the production staff, Katherine Tozer, Sharon Wilkey, Ellen Troutman-Zaig, and Rebecca Demarest, for your dedication, professionalism, and attention to detail. This book is immeasurably improved thanks to you. -// TODO thanks to rest of OR team. - Any errors remaining in the book are our own, naturally. diff --git a/theme/asciidoctor-clean.custom.css b/theme/asciidoctor-clean.custom.css index 00a945c5..ac11c462 100644 --- a/theme/asciidoctor-clean.custom.css +++ b/theme/asciidoctor-clean.custom.css @@ -89,3 +89,4 @@ table { width: 55vw!important; font-size: 3vw; } +}