├── .gitignore
├── .gitmodules
├── .travis.yml
├── Makefile
├── Readme.md
├── appendix_csvs.asciidoc
├── appendix_django.asciidoc
├── appendix_ds1_table.asciidoc
├── appendix_project_structure.asciidoc
├── appendix_validation.asciidoc
├── atlas.json
├── author_bio.html
├── book.asciidoc
├── callouts
├── 1.pdf
├── 1.png
├── 10.pdf
├── 10.png
├── 11.pdf
├── 11.png
├── 12.pdf
├── 12.png
├── 13.pdf
├── 13.png
├── 14.pdf
├── 14.png
├── 15.pdf
├── 15.png
├── 16.pdf
├── 16.png
├── 17.pdf
├── 17.png
├── 18.pdf
├── 18.png
├── 19.pdf
├── 19.png
├── 2.pdf
├── 2.png
├── 20.pdf
├── 20.png
├── 21.pdf
├── 21.png
├── 22.pdf
├── 22.png
├── 23.pdf
├── 23.png
├── 24.pdf
├── 24.png
├── 25.pdf
├── 25.png
├── 26.pdf
├── 26.png
├── 27.pdf
├── 27.png
├── 28.pdf
├── 28.png
├── 29.pdf
├── 29.png
├── 3.pdf
├── 3.png
├── 30.pdf
├── 30.png
├── 31.pdf
├── 31.png
├── 32.pdf
├── 32.png
├── 33.pdf
├── 33.png
├── 34.pdf
├── 34.png
├── 35.pdf
├── 35.png
├── 36.pdf
├── 36.png
├── 37.pdf
├── 37.png
├── 38.pdf
├── 38.png
├── 39.pdf
├── 39.png
├── 4.pdf
├── 4.png
├── 5.pdf
├── 5.png
├── 6.pdf
├── 6.png
├── 7.pdf
├── 7.png
├── 8.pdf
├── 8.png
├── 9.pdf
└── 9.png
├── chapter_01_domain_model.asciidoc
├── chapter_02_repository.asciidoc
├── chapter_03_abstractions.asciidoc
├── chapter_04_service_layer.asciidoc
├── chapter_05_high_gear_low_gear.asciidoc
├── chapter_06_uow.asciidoc
├── chapter_07_aggregate.asciidoc
├── chapter_08_events_and_message_bus.asciidoc
├── chapter_09_all_messagebus.asciidoc
├── chapter_10_commands.asciidoc
├── chapter_11_external_events.asciidoc
├── chapter_12_cqrs.asciidoc
├── chapter_13_dependency_injection.asciidoc
├── chapters.py
├── checkout-branches-for-ci.py
├── colo.html
├── copyright.html
├── cover.html
├── epilogue_1_how_to_get_there_from_here.asciidoc
├── fix-branches.py
├── images
├── C4.puml
├── C4_Component.puml
├── C4_Container.puml
├── C4_Context.puml
├── apwp_0001.png
├── apwp_0002.png
├── apwp_0101.png
├── apwp_0102.png
├── apwp_0103.png
├── apwp_0104.png
├── apwp_0201.png
├── apwp_0202.png
├── apwp_0203.png
├── apwp_0204.png
├── apwp_0205.png
├── apwp_0206.png
├── apwp_0301.png
├── apwp_0302.png
├── apwp_0401.png
├── apwp_0402.png
├── apwp_0403.png
├── apwp_0404.png
├── apwp_0405.png
├── apwp_0501.png
├── apwp_0601.png
├── apwp_0602.png
├── apwp_0701.png
├── apwp_0702.png
├── apwp_0703.png
├── apwp_0704.png
├── apwp_0705.png
├── apwp_0801.png
├── apwp_0901.png
├── apwp_0902.png
├── apwp_0903.png
├── apwp_0904.png
├── apwp_1101.png
├── apwp_1102.png
├── apwp_1103.png
├── apwp_1104.png
├── apwp_1105.png
├── apwp_1106.png
├── apwp_1201.png
├── apwp_1202.png
├── apwp_1301.png
├── apwp_1302.png
├── apwp_1303.png
├── apwp_aa01.png
├── apwp_ep01.png
├── apwp_ep02.png
├── apwp_ep03.png
├── apwp_ep04.png
├── apwp_ep05.png
├── apwp_ep06.png
├── apwp_p101.png
├── apwp_p201.png
└── cover.png
├── introduction.asciidoc
├── ix.html
├── license.txt
├── maps.drawio
├── mypy.ini
├── outline.md
├── part1.asciidoc
├── part2.asciidoc
├── plantuml.cfg
├── preface.asciidoc
├── print_figure_numbers_xref_to_image_filenames.py
├── proposal.md
├── push-branches.py
├── pytest.ini
├── rebase-appendices.sh
├── rebase-chapters.sh
├── render-diagrams.py
├── renumber-chapters.py
├── requirements.txt
├── reset-exercise-branches.py
├── tests.py
├── theme
├── asciidoctor-clean.custom.css
├── epub
│ ├── epub.css
│ ├── epub.xsl
│ └── layout.html
├── mobi
│ ├── layout.html
│ ├── mobi.css
│ └── mobi.xsl
└── pdf
│ ├── pdf.css
│ └── pdf.xsl
├── titlepage.html
├── toc.html
├── tools
├── figure_renaming_report.tsv
└── intakereport.txt
├── travis-deploy-key.enc
├── update-exercise-branch.py
└── uppercase-titles.py
/.gitignore:
--------------------------------------------------------------------------------
1 | chapter_*.html
2 | appendix_*.html
3 | preface.html
4 | introduction.html
5 | .venv
6 | .mypy_cache
7 | .env
8 | abstractions.html
9 | part2.html
10 | part1.html
11 | acknowledgements.html
12 | .asciidoctor
13 | epilogue_1_how_to_get_there_from_here.html
14 | epilogue_2_footguns.html
15 | images/*.html
16 | .idea/
17 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "code"]
2 | path = code
3 | url = git@github.com:cosmicpython/code.git
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: xenial
2 | language: python
3 | python: 3.8
4 | install:
5 | - gem install asciidoctor coderay
6 | - pip install -r requirements.txt
7 | script:
8 | - make html update-code test
9 | git:
10 | submodules: false
11 | before_install:
12 | - sudo apt-get install -y tree python-pygments
13 | - sed -i s_git@github.com:_https://github.com/_ .gitmodules
14 | - git submodule update --init --recursive
15 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | html:
2 | asciidoctor \
3 | -a stylesheet=theme/asciidoctor-clean.custom.css \
4 | -a source-highlighter=pygments \
5 | -a pygments-style=friendly \
6 | -a '!example-caption' \
7 | -a sectanchors \
8 | *.asciidoc
9 |
10 | test: html
11 | pytest tests.py --tb=short -vv
12 |
13 | update-code:
14 | # git submodule update --init --recursive
15 | cd code && git fetch
16 | ./checkout-branches-for-ci.py
17 |
18 | count-todos:
19 | ls *.asciidoc | xargs grep -c TODO | sed s/:/\\t/
20 |
21 | diagrams: html
22 | ./render-diagrams.py $(CHAP)
23 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # Book repo
2 |
3 | | Book | Code |
4 | | ---- | ---- |
5 | | [](https://travis-ci.org/cosmicpython/book) | [](https://travis-ci.org/cosmicpython/code) |
6 |
7 |
8 | ## Table of Contents
9 | 目录
10 |
11 | O'Reilly have generously said that we will be able to publish this book under a [CC license](license.txt),
12 | In the meantime, pull requests, typofixes, and more substantial feedback + suggestions are enthusiastically solicited.
13 |
14 | O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布本书。
15 | 与此同时,我们热情欢迎有关拉取请求、错别字修正以及更深入的反馈与建议。
16 |
17 | | Chapter 章节 | |
18 | |--------------------------------------------------------------------------------------------------------------------------| ----- |
19 | | [Preface 前言(已翻译)](preface.asciidoc) | |
20 | | [Introduction: Why do our designs go wrong? 引言:为什么我们的设计会出问题?(已翻译)](introduction.asciidoc) | ||
21 | | [**Part 1 Intro 第一部分简介(已翻译)**](part1.asciidoc) | |
22 | | [Chapter 1: Domain Model 第一章:领域模型(已翻译)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
23 | | [Chapter 2: Repository 第二章:仓储(已翻译)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
24 | | [Chapter 3: Interlude: Abstractions 第三章:插曲:抽象(已翻译)](chapter_03_abstractions.asciidoc) | |
25 | | [Chapter 4: Service Layer (and Flask API) 第四章:服务层(和 Flask API)(已翻译)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
26 | | [Chapter 5: TDD in High Gear and Low Gear 第五章:高速档与低速档中的测试驱动开发(TDD)(已翻译)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
27 | | [Chapter 6: Unit of Work 第六章:工作单元(已翻译)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
28 | | [Chapter 7: Aggregates 第七章:聚合(已翻译)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
29 | | [**Part 2 Intro 第二部分简介(已翻译)**](part2.asciidoc) | |
30 | | [Chapter 8: Domain Events and a Simple Message Bus 第八章:领域事件与简单消息总线(已翻译)](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
31 | | [Chapter 9: Going to Town on the MessageBus 第九章:深入探讨消息总线(已翻译)](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
32 | | [Chapter 10: Commands 第十章:命令(已翻译)](chapter_10_commands.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
33 | | [Chapter 11: External Events for Integration 第十一章:集成外部事件(已翻译)](chapter_11_external_events.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
34 | | [Chapter 12: CQRS 第十二章:命令查询责任分离(已翻译)](chapter_12_cqrs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
35 | | [Chapter 13: Dependency Injection 第十三章:依赖注入(已翻译)](chapter_13_dependency_injection.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
36 | | [Epilogue: How do I get there from here? 尾声:我该如何开始?(已翻译)](epilogue_1_how_to_get_there_from_here.asciidoc) | |
37 | | [Appendix A: Recap table 附录A:总结表格(已翻译)](appendix_ds1_table.asciidoc) | |
38 | | [Appendix B: Project Structure 附录B:项目结构(已翻译)](appendix_project_structure.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
39 | | [Appendix C: A major infrastructure change, made easy 附录C:轻松替换重要的基础设施(已翻译)](appendix_csvs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
40 | | [Appendix D: Django 附录D:Django(已翻译)](appendix_django.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
41 | | [Appendix F: Validation 附录F:校验(已翻译)](appendix_validation.asciidoc) | |
42 |
43 |
44 |
45 |
46 | Below is just instructions for me and bob really.
47 |
48 | ## Dependencies:
49 |
50 | * asciidoctor
51 | * Pygments (for syntax higlighting)
52 | * asciidoctor-diagram (to render images from the text sources in [`./images`](./images))
53 |
54 | ```sh
55 | gem install asciidoctor
56 | python2 -m pip install --user pygments
57 | gem install pygments.rb
58 | gem install asciidoctor-diagram
59 | ```
60 |
61 |
62 | ## Commands
63 |
64 | ```sh
65 | make html # builds local .html versions of each chapter
66 | make test # does a sanity-check of the code listings
67 | ```
68 |
69 |
--------------------------------------------------------------------------------
/appendix_csvs.asciidoc:
--------------------------------------------------------------------------------
1 | [[appendix_csvs]]
2 | [appendix]
3 | == Swapping Out the Infrastructure: [.keep-together]#Do Everything with CSVs#
4 | 更换基础设施:用CSV完成一切
5 |
6 | ((("CSVs, doing everything with", id="ix_CSV")))
7 | This appendix is intended as a little illustration of the benefits of the
8 | Repository, Unit of Work, and Service Layer patterns. It's intended to
9 | follow from <>.
10 |
11 | 本附录旨在稍作说明 _仓储_、工作单元和服务层模式的优势。它是为了衔接<>的内容。
12 |
13 | Just as we finish building out our Flask API and getting it ready for release,
14 | the business comes to us apologetically, saying they're not ready to use our API
15 | and asking if we could build a thing that reads just batches and orders from a couple of
16 | CSVs and outputs a third CSV with allocations.
17 |
18 | 就在我们完成 _Flask_ API 的构建并准备发布时,业务团队带着歉意找到我们,说他们还没准备好使用我们的 API,
19 | 并询问我们是否能构建一个能够从几个 CSV 中读取批次和订单数据,并输出第三个包含分配结果的 CSV 的工具。
20 |
21 | Ordinarily this is the kind of thing that might have a team cursing and spitting
22 | and making notes for their memoirs. But not us! Oh no, we've ensured that
23 | our infrastructure concerns are nicely decoupled from our domain model and
24 | service layer. Switching to CSVs will be a simple matter of writing a couple
25 | of new `Repository` and `UnitOfWork` classes, and then we'll be able to reuse
26 | _all_ of our logic from the domain layer and the service layer.
27 |
28 | 通常情况下,这种需求可能会让团队咒骂连连、怒气冲天,并将其记入他们的回忆录。但我们不一样!哦不,
29 | 我们已经确保我们的基础设施逻辑与领域模型和服务层完美解耦。切换到 CSV 只需要编写几个新的 `仓储` 和 `工作单元` 类就可以了,
30 | 之后我们就能够重用领域层和服务层的 _所有_ 逻辑。
31 |
32 | Here's an E2E test to show you how the CSVs flow in and out:
33 |
34 | 下面是一个端到端(E2E)测试,向你展示 CSV 数据是如何流入和流出的:
35 |
36 | [[first_csv_test]]
37 | .A first CSV test (tests/e2e/test_csv.py)(第一个 CSV 测试)
38 | ====
39 | [source,python]
40 | ----
41 | def test_cli_app_reads_csvs_with_batches_and_orders_and_outputs_allocations(make_csv):
42 | sku1, sku2 = random_ref("s1"), random_ref("s2")
43 | batch1, batch2, batch3 = random_ref("b1"), random_ref("b2"), random_ref("b3")
44 | order_ref = random_ref("o")
45 | make_csv("batches.csv", [
46 | ["ref", "sku", "qty", "eta"],
47 | [batch1, sku1, 100, ""],
48 | [batch2, sku2, 100, "2011-01-01"],
49 | [batch3, sku2, 100, "2011-01-02"],
50 | ])
51 | orders_csv = make_csv("orders.csv", [
52 | ["orderid", "sku", "qty"],
53 | [order_ref, sku1, 3],
54 | [order_ref, sku2, 12],
55 | ])
56 |
57 | run_cli_script(orders_csv.parent)
58 |
59 | expected_output_csv = orders_csv.parent / "allocations.csv"
60 | with open(expected_output_csv) as f:
61 | rows = list(csv.reader(f))
62 | assert rows == [
63 | ["orderid", "sku", "qty", "batchref"],
64 | [order_ref, sku1, "3", batch1],
65 | [order_ref, sku2, "12", batch2],
66 | ]
67 | ----
68 | ====
69 |
70 | Diving in and implementing without thinking about repositories and all
71 | that jazz, you might start with something like this:
72 |
73 | 如果不考虑 _仓储_ 等各种模式,直接开始实现,你可能会从类似这样的代码入手:
74 |
75 |
76 | [[first_cut_csvs]]
77 | .A first cut of our CSV reader/writer (src/bin/allocate-from-csv)(CSV 读写器的初步实现)
78 | ====
79 | [source,python]
80 | [role="non-head"]
81 | ----
82 | #!/usr/bin/env python
83 | import csv
84 | import sys
85 | from datetime import datetime
86 | from pathlib import Path
87 |
88 | from allocation.domain import model
89 |
90 |
91 | def load_batches(batches_path):
92 | batches = []
93 | with batches_path.open() as inf:
94 | reader = csv.DictReader(inf)
95 | for row in reader:
96 | if row["eta"]:
97 | eta = datetime.strptime(row["eta"], "%Y-%m-%d").date()
98 | else:
99 | eta = None
100 | batches.append(
101 | model.Batch(
102 | ref=row["ref"], sku=row["sku"], qty=int(row["qty"]), eta=eta
103 | )
104 | )
105 | return batches
106 |
107 |
108 | def main(folder):
109 | batches_path = Path(folder) / "batches.csv"
110 | orders_path = Path(folder) / "orders.csv"
111 | allocations_path = Path(folder) / "allocations.csv"
112 |
113 | batches = load_batches(batches_path)
114 |
115 | with orders_path.open() as inf, allocations_path.open("w") as outf:
116 | reader = csv.DictReader(inf)
117 | writer = csv.writer(outf)
118 | writer.writerow(["orderid", "sku", "batchref"])
119 | for row in reader:
120 | orderid, sku = row["orderid"], row["sku"]
121 | qty = int(row["qty"])
122 | line = model.OrderLine(orderid, sku, qty)
123 | batchref = model.allocate(line, batches)
124 | writer.writerow([line.orderid, line.sku, batchref])
125 |
126 |
127 | if __name__ == "__main__":
128 | main(sys.argv[1])
129 | ----
130 | ====
131 |
132 | //TODO: too much vertical whitespace in this listing
133 |
134 | It's not looking too bad! And we're reusing our domain model objects
135 | and our domain service.
136 |
137 | 看起来还不错!而且我们复用了领域模型对象和领域服务。
138 |
139 | But it's not going to work. Existing allocations need to also be part
140 | of our permanent CSV storage. We can write a second test to force us to improve
141 | things:
142 |
143 | 但这行不通。现有的分配也需要成为我们永久 CSV 存储的一部分。我们可以编写第二个测试来促使我们改进:
144 |
145 | [[second_csv_test]]
146 | .And another one, with existing allocations (tests/e2e/test_csv.py)(另一个现有分配的测试)
147 | ====
148 | [source,python]
149 | ----
150 | def test_cli_app_also_reads_existing_allocations_and_can_append_to_them(make_csv):
151 | sku = random_ref("s")
152 | batch1, batch2 = random_ref("b1"), random_ref("b2")
153 | old_order, new_order = random_ref("o1"), random_ref("o2")
154 | make_csv("batches.csv", [
155 | ["ref", "sku", "qty", "eta"],
156 | [batch1, sku, 10, "2011-01-01"],
157 | [batch2, sku, 10, "2011-01-02"],
158 | ])
159 | make_csv("allocations.csv", [
160 | ["orderid", "sku", "qty", "batchref"],
161 | [old_order, sku, 10, batch1],
162 | ])
163 | orders_csv = make_csv("orders.csv", [
164 | ["orderid", "sku", "qty"],
165 | [new_order, sku, 7],
166 | ])
167 |
168 | run_cli_script(orders_csv.parent)
169 |
170 | expected_output_csv = orders_csv.parent / "allocations.csv"
171 | with open(expected_output_csv) as f:
172 | rows = list(csv.reader(f))
173 | assert rows == [
174 | ["orderid", "sku", "qty", "batchref"],
175 | [old_order, sku, "10", batch1],
176 | [new_order, sku, "7", batch2],
177 | ]
178 | ----
179 | ====
180 |
181 |
182 | And we could keep hacking about and adding extra lines to that `load_batches` function,
183 | 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.
184 |
185 | 我们可以继续不断折腾,在 `load_batches` 函数中添加额外的代码,以及某种方式来跟踪和保存新的分配——但我们已经
186 | 有一个现成的模型来处理这些问题了!这就是我们的 _仓储_ 和工作单元模式。
187 |
188 | All we need to do ("all we need to do") is reimplement those same abstractions, but
189 | with CSVs underlying them instead of a database. And as you'll see, it really is relatively straightforward.
190 |
191 | 我们所需要做的(“我们所需要做的”)只是重新实现这些相同的抽象,但用 CSV 作为其底层存储,而不是数据库。
192 | 正如你将看到的,这实际上相对来说相当简单。
193 |
194 |
195 | === Implementing a Repository and Unit of Work for CSVs
196 | 为 CSV 实现一个 _仓储_ 和工作单元
197 |
198 |
199 | ((("repositories", "CSV-based repository")))
200 | Here's what a CSV-based repository could look like. It abstracts away all the
201 | logic for reading CSVs from disk, including the fact that it has to read _two
202 | different CSVs_ (one for batches and one for allocations), and it gives us just
203 | the familiar `.list()` API, which provides the illusion of an in-memory
204 | collection of domain objects:
205 |
206 | 以下是一个基于 CSV 的 _仓储_ 的实现示例。它抽象了从磁盘读取 CSV 的所有逻辑,
207 | 包括必须读取 _两个不同的 CSV_ (一个用于批次,一个用于分配)的事实,并为我们提供了熟悉的 `.list()` API,
208 | 这营造出一个内存中领域对象集合的假象:
209 |
210 | [[csv_repository]]
211 | .A repository that uses CSV as its storage mechanism (src/allocation/service_layer/csv_uow.py)(一个使用 CSV 作为存储机制的仓储)
212 | ====
213 | [source,python]
214 | ----
215 | class CsvRepository(repository.AbstractRepository):
216 | def __init__(self, folder):
217 | self._batches_path = Path(folder) / "batches.csv"
218 | self._allocations_path = Path(folder) / "allocations.csv"
219 | self._batches = {} # type: Dict[str, model.Batch]
220 | self._load()
221 |
222 | def get(self, reference):
223 | return self._batches.get(reference)
224 |
225 | def add(self, batch):
226 | self._batches[batch.reference] = batch
227 |
228 | def _load(self):
229 | with self._batches_path.open() as f:
230 | reader = csv.DictReader(f)
231 | for row in reader:
232 | ref, sku = row["ref"], row["sku"]
233 | qty = int(row["qty"])
234 | if row["eta"]:
235 | eta = datetime.strptime(row["eta"], "%Y-%m-%d").date()
236 | else:
237 | eta = None
238 | self._batches[ref] = model.Batch(ref=ref, sku=sku, qty=qty, eta=eta)
239 | if self._allocations_path.exists() is False:
240 | return
241 | with self._allocations_path.open() as f:
242 | reader = csv.DictReader(f)
243 | for row in reader:
244 | batchref, orderid, sku = row["batchref"], row["orderid"], row["sku"]
245 | qty = int(row["qty"])
246 | line = model.OrderLine(orderid, sku, qty)
247 | batch = self._batches[batchref]
248 | batch._allocations.add(line)
249 |
250 | def list(self):
251 | return list(self._batches.values())
252 | ----
253 | ====
254 |
255 | // TODO (hynek) re self._load(): DUDE! no i/o in init!
256 |
257 |
258 | ((("Unit of Work pattern", "UoW for CSVs")))
259 | And here's what a UoW for CSVs would look like:
260 |
261 | 以下是基于 CSV 的工作单元 (UoW) 的实现示例:
262 |
263 |
264 |
265 | [[csvs_uow]]
266 | .A UoW for CSVs: commit = csv.writer (src/allocation/service_layer/csv_uow.py)(基于 CSV 的工作单元:commit = csv.writer)
267 | ====
268 | [source,python]
269 | ----
270 | class CsvUnitOfWork(unit_of_work.AbstractUnitOfWork):
271 | def __init__(self, folder):
272 | self.batches = CsvRepository(folder)
273 |
274 | def commit(self):
275 | with self.batches._allocations_path.open("w") as f:
276 | writer = csv.writer(f)
277 | writer.writerow(["orderid", "sku", "qty", "batchref"])
278 | for batch in self.batches.list():
279 | for line in batch._allocations:
280 | writer.writerow(
281 | [line.orderid, line.sku, line.qty, batch.reference]
282 | )
283 |
284 | def rollback(self):
285 | pass
286 | ----
287 | ====
288 |
289 |
290 | And once we have that, our CLI app for reading and writing batches
291 | and allocations to CSV is pared down to what it should be—a bit
292 | of code for reading order lines, and a bit of code that invokes our
293 | _existing_ service layer:
294 |
295 | 一旦我们实现了这些,我们的 CLI 应用程序,用于读取和写入批次和分配到 CSV,就可以被简化为它应有的样子——一些用于读取订单项的代码,
296 | 以及一些调用我们 _现有_ 服务层的代码:
297 |
298 | [role="nobreakinside less_space"]
299 | [[final_cli]]
300 | .Allocation with CSVs in nine lines (src/bin/allocate-from-csv)(九行代码实现用 CSV 进行分配)
301 | ====
302 | [source,python]
303 | ----
304 | def main(folder):
305 | orders_path = Path(folder) / "orders.csv"
306 | uow = csv_uow.CsvUnitOfWork(folder)
307 | with orders_path.open() as f:
308 | reader = csv.DictReader(f)
309 | for row in reader:
310 | orderid, sku = row["orderid"], row["sku"]
311 | qty = int(row["qty"])
312 | services.allocate(orderid, sku, qty, uow)
313 | ----
314 | ====
315 |
316 |
317 | ((("CSVs, doing everything with", startref="ix_CSV")))
318 | Ta-da! _Now are y'all impressed or what_?
319 |
320 | 瞧! _现在你们是不是感到惊叹了?_
321 |
322 | Much love,
323 |
324 | 满怀敬意,
325 |
326 | Bob and Harry
327 |
328 | Bob 和 Harry
329 |
--------------------------------------------------------------------------------
/appendix_django.asciidoc:
--------------------------------------------------------------------------------
1 | [[appendix_django]]
2 | [appendix]
3 | == Repository and Unit of Work [.keep-together]#Patterns with Django#
4 | 在 Django 中使用 _仓储_ 和工作单元模式
5 |
6 | ((("Django", "installing")))
7 | ((("Django", id="ix_Django")))
8 | Suppose you wanted to use Django instead of SQLAlchemy and Flask. How
9 | might things look? The first thing is to choose where to install it. We put it in a separate
10 | package next to our main allocation code:
11 |
12 | 假设你想使用 Django 来替代 SQLAlchemy 和 Flask。那么,应该如何实现呢?首先,需要选择在哪里安装它。
13 | 我们将其放在一个与我们的主要分配代码相邻的独立包中:
14 |
15 |
16 | [[django_tree]]
17 | ====
18 | [source,text]
19 | [role="tree"]
20 | ----
21 | ├── src
22 | │ ├── allocation
23 | │ │ ├── __init__.py
24 | │ │ ├── adapters
25 | │ │ │ ├── __init__.py
26 | ...
27 | │ ├── djangoproject
28 | │ │ ├── alloc
29 | │ │ │ ├── __init__.py
30 | │ │ │ ├── apps.py
31 | │ │ │ ├── migrations
32 | │ │ │ │ ├── 0001_initial.py
33 | │ │ │ │ └── __init__.py
34 | │ │ │ ├── models.py
35 | │ │ │ └── views.py
36 | │ │ ├── django_project
37 | │ │ │ ├── __init__.py
38 | │ │ │ ├── settings.py
39 | │ │ │ ├── urls.py
40 | │ │ │ └── wsgi.py
41 | │ │ └── manage.py
42 | │ └── setup.py
43 | └── tests
44 | ├── conftest.py
45 | ├── e2e
46 | │ └── test_api.py
47 | ├── integration
48 | │ ├── test_repository.py
49 | ...
50 | ----
51 | ====
52 |
53 |
54 | [TIP]
55 | ====
56 | The code for this appendix is in the
57 | appendix_django branch https://oreil.ly/A-I76[on GitHub]:
58 |
59 | 本附录的代码位于
60 | appendix_django 分支 https://oreil.ly/A-I76[在 GitHub 上]:
61 |
62 | ----
63 | git clone https://github.com/cosmicpython/code.git
64 | cd code
65 | git checkout appendix_django
66 | ----
67 |
68 | Code examples follows on from the end of <>.
69 |
70 | 代码示例接续自 <> 的结尾。
71 |
72 | ====
73 |
74 |
75 | === Repository Pattern with Django
76 | 使用 Django 的仓储模式
77 |
78 | ((("pytest", "pytest-django plug-in")))
79 | ((("Repository pattern", "with Django", id="ix_RepoDjango")))
80 | ((("Django", "Repository pattern with", id="ix_DjangoRepo")))
81 | We used a plugin called
82 | https://github.com/pytest-dev/pytest-django[`pytest-django`] to help with test
83 | database management.
84 |
85 | 我们使用了一个名为 https://github.com/pytest-dev/pytest-django[`pytest-django`] 的插件来帮助管理测试数据库。
86 |
87 | Rewriting the first repository test was a minimal change—just rewriting
88 | some raw SQL with a call to the Django ORM/QuerySet language:
89 |
90 | 重写第一个仓储测试是一个最小化的改动——只是用调用 Django ORM/QuerySet 语言来重写了一些原始 SQL:
91 |
92 |
93 | [[django_repo_test1]]
94 | .First repository test adapted (tests/integration/test_repository.py)(调整后的第一个仓储测试)
95 | ====
96 | [source,python]
97 | ----
98 | from djangoproject.alloc import models as django_models
99 |
100 |
101 | @pytest.mark.django_db
102 | def test_repository_can_save_a_batch():
103 | batch = model.Batch("batch1", "RUSTY-SOAPDISH", 100, eta=date(2011, 12, 25))
104 |
105 | repo = repository.DjangoRepository()
106 | repo.add(batch)
107 |
108 | [saved_batch] = django_models.Batch.objects.all()
109 | assert saved_batch.reference == batch.reference
110 | assert saved_batch.sku == batch.sku
111 | assert saved_batch.qty == batch._purchased_quantity
112 | assert saved_batch.eta == batch.eta
113 | ----
114 | ====
115 |
116 |
117 | The second test is a bit more involved since it has allocations,
118 | but it is still made up of familiar-looking Django code:
119 |
120 | 第二个测试稍微复杂一些,因为它涉及分配,但它仍然由看起来熟悉的 Django 代码组成:
121 |
122 | [[django_repo_test2]]
123 | .Second repository test is more involved (tests/integration/test_repository.py)(第二个仓储测试更加复杂)
124 | ====
125 | [source,python]
126 | ----
127 | @pytest.mark.django_db
128 | def test_repository_can_retrieve_a_batch_with_allocations():
129 | sku = "PONY-STATUE"
130 | d_line = django_models.OrderLine.objects.create(orderid="order1", sku=sku, qty=12)
131 | d_batch1 = django_models.Batch.objects.create(
132 | reference="batch1", sku=sku, qty=100, eta=None
133 | )
134 | d_batch2 = django_models.Batch.objects.create(
135 | reference="batch2", sku=sku, qty=100, eta=None
136 | )
137 | django_models.Allocation.objects.create(line=d_line, batch=d_batch1)
138 |
139 | repo = repository.DjangoRepository()
140 | retrieved = repo.get("batch1")
141 |
142 | expected = model.Batch("batch1", sku, 100, eta=None)
143 | assert retrieved == expected # Batch.__eq__ only compares reference
144 | assert retrieved.sku == expected.sku
145 | assert retrieved._purchased_quantity == expected._purchased_quantity
146 | assert retrieved._allocations == {
147 | model.OrderLine("order1", sku, 12),
148 | }
149 | ----
150 | ====
151 |
152 | Here's how the actual repository ends up looking:
153 |
154 | 实际的仓储最终如下所示:
155 |
156 |
157 | [[django_repository]]
158 | .A Django repository (src/allocation/adapters/repository.py)(一个 Django 仓储)
159 | ====
160 | [source,python]
161 | ----
162 | class DjangoRepository(AbstractRepository):
163 | def add(self, batch):
164 | super().add(batch)
165 | self.update(batch)
166 |
167 | def update(self, batch):
168 | django_models.Batch.update_from_domain(batch)
169 |
170 | def _get(self, reference):
171 | return (
172 | django_models.Batch.objects.filter(reference=reference)
173 | .first()
174 | .to_domain()
175 | )
176 |
177 | def list(self):
178 | return [b.to_domain() for b in django_models.Batch.objects.all()]
179 | ----
180 | ====
181 |
182 |
183 | You can see that the implementation relies on the Django models having
184 | some custom methods for translating to and from our domain model.footnote:[
185 | The DRY-Python project people have built a tool called
186 | https://mappers.readthedocs.io/en/latest[mappers] that looks like it might
187 | help minimize boilerplate for this sort of thing.]
188 |
189 | 你可以看到,该实现依赖于 Django 模型中一些自定义方法来在我们的领域模型之间进行转换。脚注:
190 | DRY-Python 项目的开发者构建了一个名为 https://mappers.readthedocs.io/en/latest[mappers] 的工具,
191 | 看起来它可能有助于减少此类代码的样板。
192 |
193 |
194 | ==== Custom Methods on Django ORM Classes to Translate to/from Our Domain Model
195 | 在 Django ORM 类上定义自定义方法用于在我们的领域模型之间进行转换
196 |
197 | ((("domain model", "Django custom ORM methods for conversion")))
198 | ((("object-relational mappers (ORMs)", "Django, custom methods to translate to/from domain model")))
199 | Those custom methods look something like this:
200 |
201 | 这些自定义方法看起来是这样的:
202 |
203 | [[django_models]]
204 | .Django ORM with custom methods for domain model conversion (src/djangoproject/alloc/models.py)(使用自定义方法进行领域模型转换的 Django ORM)
205 | ====
206 | [source,python]
207 | ----
208 | from django.db import models
209 | from allocation.domain import model as domain_model
210 |
211 |
212 | class Batch(models.Model):
213 | reference = models.CharField(max_length=255)
214 | sku = models.CharField(max_length=255)
215 | qty = models.IntegerField()
216 | eta = models.DateField(blank=True, null=True)
217 |
218 | @staticmethod
219 | def update_from_domain(batch: domain_model.Batch):
220 | try:
221 | b = Batch.objects.get(reference=batch.reference) #<1>
222 | except Batch.DoesNotExist:
223 | b = Batch(reference=batch.reference) #<1>
224 | b.sku = batch.sku
225 | b.qty = batch._purchased_quantity
226 | b.eta = batch.eta #<2>
227 | b.save()
228 | b.allocation_set.set(
229 | Allocation.from_domain(l, b) #<3>
230 | for l in batch._allocations
231 | )
232 |
233 | def to_domain(self) -> domain_model.Batch:
234 | b = domain_model.Batch(
235 | ref=self.reference, sku=self.sku, qty=self.qty, eta=self.eta
236 | )
237 | b._allocations = set(
238 | a.line.to_domain()
239 | for a in self.allocation_set.all()
240 | )
241 | return b
242 |
243 |
244 | class OrderLine(models.Model):
245 | #...
246 | ----
247 | ====
248 |
249 | <1> For value objects, `objects.get_or_create` can work, but for entities,
250 | you probably need an explicit try-get/except to handle the upsert.footnote:[
251 | `@mr-bo-jangles` suggested you might be able to use https://oreil.ly/HTq1r[`update_or_create`],
252 | but that's beyond our Django-fu.]
253 | 对于值对象,`objects.get_or_create` 可以正常工作,但对于实体,你可能需要显式的 try-get/except 来处理 upsert(更新或插入)。脚注:
254 | `@mr-bo-jangles` 提出你或许可以使用 https://oreil.ly/HTq1r[`update_or_create`],但这超出了我们对 Django 的掌握范围。
255 |
256 | <2> We've shown the most complex example here. If you do decide to do this,
257 | be aware that there will be boilerplate! Thankfully it's not very
258 | complex boilerplate.
259 | 我们在这里展示了最复杂的示例。如果你决定这样做,请注意会有一些样板代码!不过值得庆幸的是,这些样板代码并不复杂。
260 |
261 | <3> Relationships also need some careful, custom handling.
262 | 关系也需要一些仔细而定制化的处理。
263 |
264 |
265 | NOTE: As in <>, we use dependency inversion.
266 | The ORM (Django) depends on the model and not the other way around.
267 | ((("Django", "Repository pattern with", startref="ix_DjangoRepo")))
268 | ((("Repository pattern", "with Django", startref="ix_RepoDjango")))
269 | 与 <> 中一样,我们使用了依赖反转原则。
270 | ORM(Django)依赖于模型,而不是反过来。
271 |
272 |
273 |
274 | === Unit of Work Pattern with Django
275 | 使用 Django 的工作单元模式
276 |
277 |
278 | ((("Django", "Unit of Work pattern with", id="ix_DjangoUoW")))
279 | ((("Unit of Work pattern", "with Django", id="ix_UoWDjango")))
280 | The tests don't change too much:
281 |
282 | 测试并没有发生太大的变化:
283 |
284 | [[test_uow_django]]
285 | .Adapted UoW tests (tests/integration/test_uow.py)(适配后的工作单元测试)
286 | ====
287 | [source,python]
288 | ----
289 | def insert_batch(ref, sku, qty, eta): #<1>
290 | django_models.Batch.objects.create(reference=ref, sku=sku, qty=qty, eta=eta)
291 |
292 |
293 | def get_allocated_batch_ref(orderid, sku): #<1>
294 | return django_models.Allocation.objects.get(
295 | line__orderid=orderid, line__sku=sku
296 | ).batch.reference
297 |
298 |
299 | @pytest.mark.django_db(transaction=True)
300 | def test_uow_can_retrieve_a_batch_and_allocate_to_it():
301 | insert_batch("batch1", "HIPSTER-WORKBENCH", 100, None)
302 |
303 | uow = unit_of_work.DjangoUnitOfWork()
304 | with uow:
305 | batch = uow.batches.get(reference="batch1")
306 | line = model.OrderLine("o1", "HIPSTER-WORKBENCH", 10)
307 | batch.allocate(line)
308 | uow.commit()
309 |
310 | batchref = get_allocated_batch_ref("o1", "HIPSTER-WORKBENCH")
311 | assert batchref == "batch1"
312 |
313 |
314 | @pytest.mark.django_db(transaction=True) #<2>
315 | def test_rolls_back_uncommitted_work_by_default():
316 | ...
317 |
318 | @pytest.mark.django_db(transaction=True) #<2>
319 | def test_rolls_back_on_error():
320 | ...
321 | ----
322 | ====
323 |
324 | <1> Because we had little helper functions in these tests, the actual
325 | main bodies of the tests are pretty much the same as they were with
326 | SQLAlchemy.
327 | 由于我们在这些测试中使用了一些辅助函数,测试的主要主体部分实际上与使用 SQLAlchemy 时几乎相同。
328 |
329 | <2> The `pytest-django` `mark.django_db(transaction=True)` is required to
330 | test our custom transaction/rollback behaviors.
331 | 为了测试我们自定义的事务/回滚行为,需要使用 `pytest-django` 的 `mark.django_db(transaction=True)`。
332 |
333 |
334 |
335 | And the implementation is quite simple, although it took me a few
336 | tries to find which invocation of Django's transaction magic
337 | would work:
338 |
339 | 实现相当简单,尽管我花了几次尝试才找到能够发挥作用的 Django 事务机制的调用方式:
340 |
341 |
342 | [[start_uow_django]]
343 | .UoW adapted for Django (src/allocation/service_layer/unit_of_work.py)(适配 Django 的工作单元)
344 | ====
345 | [source,python]
346 | ----
347 | class DjangoUnitOfWork(AbstractUnitOfWork):
348 | def __enter__(self):
349 | self.batches = repository.DjangoRepository()
350 | transaction.set_autocommit(False) #<1>
351 | return super().__enter__()
352 |
353 | def __exit__(self, *args):
354 | super().__exit__(*args)
355 | transaction.set_autocommit(True)
356 |
357 | def commit(self):
358 | for batch in self.batches.seen: #<3>
359 | self.batches.update(batch) #<3>
360 | transaction.commit() #<2>
361 |
362 | def rollback(self):
363 | transaction.rollback() #<2>
364 | ----
365 | ====
366 |
367 | <1> `set_autocommit(False)` was the best way to tell Django to stop
368 | automatically committing each ORM operation immediately, and to
369 | begin a transaction.
370 | `set_autocommit(False)` 是告诉 Django 停止立即自动提交每次 ORM 操作并开始一个事务的最佳方式。
371 |
372 | <2> Then we use the explicit rollback and commits.
373 | 然后我们使用显式的回滚和提交操作。
374 |
375 | <3> One difficulty: because, unlike with SQLAlchemy, we're not
376 | instrumenting the domain model instances themselves, the
377 | `commit()` command needs to explicitly go through all the
378 | objects that have been touched by every repository and manually
379 | update them back to the ORM.
380 | ((("Django", "Unit of Work pattern with", startref="ix_DjangoUoW")))
381 | ((("Unit of Work pattern", "with Django", startref="ix_UoWDjango")))
382 | 一个难点是:与使用 SQLAlchemy 不同,我们并没有对领域模型实例本身进行操作,因此 `commit()` 命令需要显式地遍历每个仓储操作过的所有对象,
383 | 并手动将它们更新回 ORM。
384 |
385 |
386 |
387 | === API: Django Views Are Adapters
388 | API:Django 视图是适配器
389 |
390 | ((("adapters", "Django views")))
391 | ((("views", "Django views as adapters")))
392 | ((("APIs", "Django views as adapters")))
393 | ((("Django", "views are adapters")))
394 | The Django _views.py_ file ends up being almost identical to the
395 | old _flask_app.py_, because our architecture means it's a very
396 | thin wrapper around our service layer (which didn't change at all, by the way):
397 |
398 | Django 的 _views.py_ 文件最终与之前的 _flask_app.py_ 几乎完全相同,
399 | 因为我们的架构使其成为服务层的一个非常薄的封装(顺便说一下,服务层完全没有改变):
400 |
401 |
402 | [[django_views]]
403 | .Flask app -> Django views (src/djangoproject/alloc/views.py)(Flask 应用程序 -> Django 视图)
404 | ====
405 | [source,python]
406 | ----
407 | os.environ["DJANGO_SETTINGS_MODULE"] = "djangoproject.django_project.settings"
408 | django.setup()
409 |
410 |
411 | @csrf_exempt
412 | def add_batch(request):
413 | data = json.loads(request.body)
414 | eta = data["eta"]
415 | if eta is not None:
416 | eta = datetime.fromisoformat(eta).date()
417 | services.add_batch(
418 | data["ref"], data["sku"], data["qty"], eta,
419 | unit_of_work.DjangoUnitOfWork(),
420 | )
421 | return HttpResponse("OK", status=201)
422 |
423 |
424 | @csrf_exempt
425 | def allocate(request):
426 | data = json.loads(request.body)
427 | try:
428 | batchref = services.allocate(
429 | data["orderid"],
430 | data["sku"],
431 | data["qty"],
432 | unit_of_work.DjangoUnitOfWork(),
433 | )
434 | except (model.OutOfStock, services.InvalidSku) as e:
435 | return JsonResponse({"message": str(e)}, status=400)
436 |
437 | return JsonResponse({"batchref": batchref}, status=201)
438 | ----
439 | ====
440 |
441 |
442 | === Why Was This All So Hard?
443 | 为什么这一切都如此困难?
444 |
445 | ((("Django", "using, difficulty of")))
446 | OK, it works, but it does feel like more effort than Flask/SQLAlchemy. Why is
447 | that?
448 |
449 | 好的,它可以工作,但确实感觉比 Flask/SQLAlchemy 更费力。为什么会这样呢?
450 |
451 | The main reason at a low level is because Django's ORM doesn't work in the same
452 | way. We don't have an equivalent of the SQLAlchemy classical mapper, so our
453 | `ActiveRecord` and our domain model can't be the same object. Instead we have to
454 | build a manual translation layer behind the repository. That's more
455 | work (although once it's done, the ongoing maintenance burden shouldn't be too
456 | high).
457 |
458 | 从底层来看,主要原因是 Django 的 ORM 工作方式不同。我们没有与 SQLAlchemy 的经典映射器等价的功能,
459 | 因此我们的 `ActiveRecord` 和领域模型不能是同一个对象。相反,我们必须在仓储后面构建一个手动的转换层。这确实需要更多的工作(不过一旦完成,
460 | 后续的维护负担应该不会太高)。
461 |
462 | ((("pytest", "pytest-django plugin")))
463 | Because Django is so tightly coupled to the database, you have to use helpers
464 | like `pytest-django` and think carefully about test databases, right from
465 | the very first line of code, in a way that we didn't have to when we started
466 | out with our pure domain model.
467 |
468 | 因为 Django 与数据库的耦合非常紧密,所以你必须使用类似 `pytest-django` 这样的辅助工具,并从第一行代码开始就仔细考虑测试数据库的设置,
469 | 这是我们在使用纯领域模型开始时所不需要处理的。
470 |
471 | But at a higher level, the entire reason that Django is so great
472 | is that it's designed around the sweet spot of making it easy to build CRUD
473 | apps with minimal boilerplate. But the entire thrust of our book is about
474 | what to do when your app is no longer a simple CRUD app.
475 |
476 | 但从更高的层面来看,Django 之所以如此出色,完全是因为它围绕着简化构建 CRUD 应用的最佳方式设计,且所需的样板代码极少。
477 | 但我们这本书的核心讨论点是,当你的应用不再是一个简单的 CRUD 应用时,该怎么办。
478 |
479 | At that point, Django starts hindering more than it helps. Things like the
480 | Django admin, which are so awesome when you start out, become actively dangerous
481 | if the whole point of your app is to build a complex set of rules and modeling
482 | around the workflow of state changes. The Django admin bypasses all of that.
483 |
484 | 此时,Django 帮助的作用开始被它带来的阻碍所抵消。像 Django Admin 这样的功能,在开始时非常出色,
485 | 但如果你的应用的核心在于围绕状态变更的工作流构建一套复杂的规则和模型,那么它就会变得极其危险。因为 Django Admin 会绕过这些规则和逻辑。
486 |
487 | === What to Do If You Already Have Django
488 | 如果你已经在使用 Django,该怎么办
489 |
490 | ((("Django", "applying patterns to Django app")))
491 | So what should you do if you want to apply some of the patterns in this book
492 | to a Django app? We'd say the following:
493 |
494 | 那么,如果你想将本书中的一些模式应用到一个 Django 应用中,你应该怎么做呢?我们建议如下:
495 |
496 | * The Repository and Unit of Work patterns are going to be quite a lot of work. The
497 | main thing they will buy you in the short term is faster unit tests, so
498 | evaluate whether that benefit feels worth it in your case. In the longer term, they
499 | decouple your app from Django and the database, so if you anticipate wanting
500 | to migrate away from either of those, Repository and UoW are a good idea.
501 | 仓储模式和工作单元模式会带来相当多的工作量。从短期来看,它们主要为你带来的好处是更快的单元测试,因此你需要评估这种好处是否对你来说值得。
502 | 从长期来看,它们会将你的应用程序与 Django 和数据库解耦,所以如果你预计可能需要从两者中的任何一个迁移开,
503 | 使用仓储模式和工作单元模式是一个不错的选择。
504 |
505 | * The Service Layer pattern might be of interest if you're seeing a lot of duplication in
506 | your _views.py_. It can be a good way of thinking about your use cases separately from your web endpoints.
507 | 如果你在 _views.py_ 文件中看到大量的代码重复,那么服务层模式可能会引起你的兴趣。它是一种将你的用例与 Web 端点分开思考的好方法。
508 |
509 | * You can still theoretically do DDD and domain modeling with Django models,
510 | tightly coupled as they are to the database; you may be slowed by
511 | migrations, but it shouldn't be fatal. So as long as your app is not too
512 | complex and your tests not too slow, you may be able to get something out of
513 | the _fat models_ approach: push as much logic down to your models as possible,
514 | and apply patterns like Entity, Value Object, and Aggregate. However, see
515 | the following caveat.
516 | 理论上,即使 Django 模型与数据库紧密耦合,你仍然可以使用 DDD(领域驱动设计)和领域建模;虽然迁移过程可能会拖慢你的进度,但这不至于致命。
517 | 所以只要你的应用程序不是太复杂,测试也不是太慢,你或许可以从 _胖模型_ 方法中获益:尽可能将逻辑下放到模型中,
518 | 并应用如实体(Entity)、值对象(Value Object)和聚合(Aggregate)等模式。然而,请注意以下的注意事项。
519 |
520 | With that said,
521 | https://oreil.ly/Nbpjj[word
522 | in the Django community] is that people find that the fat models approach runs into
523 | scalability problems of its own, particularly around managing interdependencies
524 | between apps. In those cases, there's a lot to be said for extracting out a
525 | business logic or domain layer to sit between your views and forms and
526 | your _models.py_, which you can then keep as minimal as possible.
527 |
528 | 话虽如此,
529 | https://oreil.ly/Nbpjj[在 Django 社区的反馈] 表明,人们发现胖模型方法本身会遇到可扩展性问题,特别是在管理应用程序之间的相互依赖方面。
530 | 在这些情况下,将业务逻辑或领域层提取出来,置于视图和表单与 _models.py_ 之间,有很多好处。而且,这也让你的 _models.py_ 可以尽量保持精简。
531 |
532 | === Steps Along the Way
533 | 渐进式的步骤
534 |
535 | ((("Django", "applying patterns to Django app", "steps along the way")))
536 | Suppose you're working on a Django project that you're not sure is going
537 | to get complex enough to warrant the patterns we recommend, but you still
538 | want to put a few steps in place to make your life easier, both in the medium
539 | term and if you want to migrate to some of our patterns later. Consider the following:
540 |
541 | 假设你正在开发一个 Django 项目,而你不确定该项目是否会变得足够复杂以至于需要使用我们推荐的模式,但你仍然希望采取一些步骤,
542 | 使你的工作在中期更轻松一些,并且如果将来想迁移到我们的一些模式也会更方便。可以考虑以下建议:
543 |
544 | * 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
545 | forms, views, and models free of business logic. It can become a stepping-stone
546 | for moving to a fully decoupled domain model and/or service layer later.
547 | 我们听过的一条建议是,从第一天开始就在每个 Django 应用中创建一个 __logic.py__ 文件。这为你提供了一个放置业务逻辑的地方,
548 | 同时可以让你的表单、视图和模型中不包含业务逻辑。它可以成为将来迁移到完全解耦的领域模型和/或服务层的一个踏脚石。
549 |
550 | * A business-logic layer might start out working with Django model objects and only later become fully decoupled from the framework and work on
551 | plain Python data structures.
552 | 业务逻辑层可能一开始是与 Django 模型对象一起工作的,而只有在之后才完全与框架解耦,转而使用纯粹的 _Python_ 数据结构。
553 |
554 | [role="pagebreak-before"]
555 | * For the read side, you can get some of the benefits of CQRS by putting reads
556 | into one place, avoiding ORM calls sprinkled all over the place.
557 | 在读取方面,你可以通过将读取操作集中到一个地方来获得一些 CQRS 的好处,避免 ORM 调用分散在各处。
558 |
559 | * When separating out modules for reads and modules for domain logic, it
560 | may be worth decoupling yourself from the Django apps hierarchy. Business
561 | concerns will cut across them.
562 | 当将读取模块和领域逻辑模块分离时,值得考虑让自己从 Django 的应用层次结构中解耦。业务需求通常会跨越这些应用模块。
563 |
564 |
565 | NOTE: We'd like to give a shout-out to David Seddon and Ashia Zawaduk for
566 | talking through some of the ideas in this appendix. They did their best to
567 | stop us from saying anything really stupid about a topic we don't really
568 | have enough personal experience of, but they may have failed.
569 | 我们要向 David Seddon 和 Ashia Zawaduk 表示感谢,感谢他们与我们一起讨论了本附录中的一些想法。
570 | 他们尽了最大的努力阻止我们在一个我们自己没有足够经验的话题上说出任何非常愚蠢的话,不过他们可能未能完全做到。
571 |
572 | ((("Django", startref="ix_Django")))
573 | For more thoughts and actual lived experience dealing with existing
574 | applications, refer to the <>.
575 |
576 | 有关处理现有应用程序的更多想法和实际经验,请参阅 <>。
577 |
--------------------------------------------------------------------------------
/appendix_ds1_table.asciidoc:
--------------------------------------------------------------------------------
1 | [[appendix_ds1_table]]
2 | [appendix]
3 | == Summary Diagram and Table
4 | 总结图表及表格
5 |
6 | ((("architecture, summary diagram and table", id="ix_archsumm")))
7 | Here's what our architecture looks like by the end of the book:
8 |
9 | 这是本书结尾时我们的架构图:
10 |
11 | [[recap_diagram]]
12 | image::images/apwp_aa01.png["diagram showing all components: flask+eventconsumer, service layer, adapters, domain etc"]
13 |
14 | <> recaps each pattern and what it does.
15 |
16 | <> 总结了每种模式及其功能。
17 |
18 | [[ds1_table]]
19 | .The components of our architecture and what they all do(我们架构的各个组件及其功能)
20 | [cols="1,1,2"]
21 | |===
22 | | Layer(层级) | Component(组件) | Description(描述)
23 |
24 | .5+a| *Domain*
25 | (*领域*)
26 |
27 | __Defines the business logic.__
28 | (__定义业务逻辑。__)
29 |
30 |
31 | | Entity(实体) | A domain object whose attributes may change but that has a recognizable identity over time.
32 | (一种领域对象,其属性可能会发生变化,但在一段时间内具有可识别的身份。)
33 |
34 | | Value object(值对象) | An immutable domain object whose attributes entirely define it. It is fungible with other identical objects.
35 | (一个不可变的领域对象,其属性完全定义了自身。它可以与其他相同的对象互换。)
36 |
37 | | Aggregate(聚合) | Cluster of associated objects that we treat as a unit for the purpose of data changes. Defines and enforces a consistency boundary.
38 | (关联对象的集合,为数据变更的目的将其视为一个整体。定义并强制执行一致性边界。)
39 |
40 | | Event(事件) | Represents something that happened.
41 | (表示已发生的某件事。)
42 |
43 | | Command(命令) | Represents a job the system should perform.
44 | (表示系统应该执行的一项任务。)
45 |
46 | .3+a| *Service Layer*(*服务层*)
47 |
48 | __Defines the jobs the system should perform and orchestrates different components.__
49 | (__定义系统应该执行的任务并协调不同的组件。__)
50 |
51 | | Handler(处理器) | Receives a command or an event and performs what needs to happen.
52 | (接收命令或事件并执行需要完成的操作。)
53 | | Unit of work(工作单元) | Abstraction around data integrity. Each unit of work represents an atomic update. Makes repositories available. Tracks new events on retrieved aggregates.
54 | (围绕数据完整性的抽象。每个工作单元表示一次原子性更新。提供仓储支持。跟踪已检索聚合上的新事件。)
55 | | Message bus (internal)(消息总线(内部)) | Handles commands and events by routing them to the appropriate handler.
56 | (通过将命令和事件路由到适当的处理器进行处理。)
57 |
58 | .2+a| *Adapters* (Secondary)
59 | (*适配器*(次级))
60 |
61 | __Concrete implementations of an interface that goes from our system
62 | to the outside world (I/O).__
63 | (__从我们的系统到外部世界(I/O)的接口的具体实现。__)
64 |
65 | | Repository(仓储) | Abstraction around persistent storage. Each aggregate has its own repository.
66 | (围绕持久化存储的抽象。每个聚合都有其自己的仓储。)
67 | | Event publisher(事件发布器) | Pushes events onto the external message bus.
68 | (将事件推送到外部消息总线。)
69 |
70 | .2+a| *Entrypoints* (Primary adapters)
71 | (*入口点*(主要适配器))
72 |
73 | __Translate external inputs into calls into the service layer.__
74 | (__将外部输入转换为对服务层的调用。__)
75 |
76 | | Web | Receives web requests and translates them into commands, passing them to the internal message bus.
77 | (接收 Web 请求并将其转换为命令,然后将其传递到内部消息总线。)
78 | | Event consumer(事件消费者) | Reads events from the external message bus and translates them into commands, passing them to the internal message bus.
79 | (从外部消息总线读取事件并将其转换为命令,然后传递到内部消息总线。)
80 |
81 | | N/A | External message bus (message broker)(外部消息总线(消息代理)) | A piece of infrastructure that different services use to intercommunicate, via events.
82 | (一个基础设施,不同的服务通过事件使用它进行相互通信。)
83 | |===
84 | ((("architecture, summary diagram and table", startref="ix_archsumm")))
85 |
--------------------------------------------------------------------------------
/appendix_project_structure.asciidoc:
--------------------------------------------------------------------------------
1 | [[appendix_project_structure]]
2 | [appendix]
3 | == A Template Project Structure
4 | 一个模板项目结构
5 |
6 | ((("projects", "template project structure", id="ix_prjstrct")))
7 | Around <>, we moved from just having
8 | everything in one folder to a more structured tree, and we thought it might
9 | be of interest to outline the moving parts.
10 |
11 | 在 <> 中,我们从将所有内容都放在一个文件夹中转向了更结构化的目录树。我们认为概述这些组成部分可能会让你感兴趣。
12 |
13 | [TIP]
14 | ====
15 | The code for this appendix is in the
16 | appendix_project_structure branch https://oreil.ly/1rDRC[on GitHub]:
17 |
18 | 本附录的代码位于 GitHub 上的 `appendix_project_structure` 分支 https://oreil.ly/1rDRC[见此处]:
19 |
20 | ----
21 | git clone https://github.com/cosmicpython/code.git
22 | cd code
23 | git checkout appendix_project_structure
24 | ----
25 | ====
26 |
27 |
28 | The basic folder structure looks like this:
29 |
30 | 基本的文件夹结构如下所示:
31 |
32 | [[project_tree]]
33 | .Project tree
34 | ====
35 | [source,text]
36 | [role="tree"]
37 | ----
38 | .
39 | ├── Dockerfile <1>
40 | ├── Makefile <2>
41 | ├── README.md
42 | ├── docker-compose.yml <1>
43 | ├── license.txt
44 | ├── mypy.ini
45 | ├── requirements.txt
46 | ├── src <3>
47 | │ ├── allocation
48 | │ │ ├── __init__.py
49 | │ │ ├── adapters
50 | │ │ │ ├── __init__.py
51 | │ │ │ ├── orm.py
52 | │ │ │ └── repository.py
53 | │ │ ├── config.py
54 | │ │ ├── domain
55 | │ │ │ ├── __init__.py
56 | │ │ │ └── model.py
57 | │ │ ├── entrypoints
58 | │ │ │ ├── __init__.py
59 | │ │ │ └── flask_app.py
60 | │ │ └── service_layer
61 | │ │ ├── __init__.py
62 | │ │ └── services.py
63 | │ └── setup.py <3>
64 | └── tests <4>
65 | ├── conftest.py <4>
66 | ├── e2e
67 | │ └── test_api.py
68 | ├── integration
69 | │ ├── test_orm.py
70 | │ └── test_repository.py
71 | ├── pytest.ini <4>
72 | └── unit
73 | ├── test_allocate.py
74 | ├── test_batches.py
75 | └── test_services.py
76 | ----
77 | ====
78 |
79 | <1> Our _docker-compose.yml_ and our _Dockerfile_ are the main bits of configuration
80 | for the containers that run our app, and they can also run the tests (for CI). A
81 | more complex project might have several Dockerfiles, although we've found that
82 | minimizing the number of images is usually a good idea.footnote:[Splitting
83 | out images for production and testing is sometimes a good idea, but we've tended
84 | to find that going further and trying to split out different images for
85 | different types of application code (e.g., Web API versus pub/sub client) usually
86 | ends up being more trouble than it's worth; the cost in terms of complexity
87 | and longer rebuild/CI times is too high. YMMV.]
88 | 我们的 _docker-compose.yml_ 和 _Dockerfile_ 是运行我们应用程序的容器的主要配置文件,它们也可以用于运行测试(用于持续集成,CI)。
89 | 一个更复杂的项目可能会有多个 Dockerfile,但我们发现,尽量减少镜像的数量通常是个好主意。脚注:分离生产与测试的镜像有时是个好主意,
90 | 但我们倾向于认为,进一步尝试为不同类型的应用程序代码(例如,Web API 和发布/订阅客户端)分离不同镜像通常会得不偿失;
91 | 这种做法在复杂性和较长的重建/CI 时间方面的成本太高。视情况而定(YMMV:Your Mileage May Vary)。
92 |
93 | <2> A __Makefile__ provides the entrypoint for all the typical commands a developer
94 | (or a CI server) might want to run during their normal workflow: `make
95 | build`, `make test`, and so on.footnote:[A pure-Python alternative to Makefiles is
96 | http://www.pyinvoke.org[Invoke], worth checking out if everyone on your
97 | team knows Python (or at least knows it better than Bash!).] This is optional. You could just use
98 | `docker-compose` and `pytest` directly, but if nothing else, it's nice to
99 | have all the "common commands" in a list somewhere, and unlike
100 | documentation, a Makefile is code so it has less tendency to become out of date.
101 | 一个 __Makefile__ 提供了所有典型命令的入口点,供开发者(或 CI 服务器)在日常工作流程中运行,比如 `make build`、`make test` 等等。
102 | 脚注:[一个纯 _Python_ 的替代方案是 http://www.pyinvoke.org[Invoke],如果你团队中的每个人都熟悉 _Python_(或至少比熟悉 Bash 更熟悉 _Python_),值得一试!]
103 | 这是可选的。你其实可以直接使用 `docker-compose` 和 `pytest`,但至少来说,把所有“常用命令”汇总在一个列表中是非常不错的。
104 | 与文档不同,Makefile 是代码,因此不太容易过时。
105 |
106 | <3> All the source code for our app, including the domain model, the
107 | Flask app, and infrastructure code, lives in a Python package inside
108 | _src_,footnote:[https://hynek.me/articles/testing-packaging["Testing and Packaging"] by Hynek Schlawack provides more information on _src_ folders.]
109 | which we install using `pip install -e` and the _setup.py_ file. This makes
110 | imports easy. Currently, the structure within this module is totally flat,
111 | but for a more complex project, you'd expect to grow a folder hierarchy
112 | that includes _domain_model/_, _infrastructure/_, _services/_, and _api/_.
113 | 我们应用程序的所有源代码,包括领域模型、 _Flask_ 应用程序和基础设施代码,都放在 _src_ 文件夹内的一个 _Python_ 包中。脚注:
114 | 关于 _src_ 文件夹的更多信息,请参考 Hynek Schlawack 的文章 https://hynek.me/articles/testing-packaging["Testing and Packaging"]。
115 | 我们使用 `pip install -e` 和 _setup.py_ 文件来安装它,这使得导入变得简单。目前,这个模块内的结构是完全扁平的,但对于更复杂的项目,
116 | 你可能需要发展出一个包含 _domain_model/_、_infrastructure/_、_services/_ 和 _api/_ 的文件夹层次结构。
117 |
118 |
119 | <4> Tests live in their own folder. Subfolders distinguish different test
120 | types and allow you to run them separately. We can keep shared fixtures
121 | (_conftest.py_) in the main tests folder and nest more specific ones if we
122 | wish. This is also the place to keep _pytest.ini_.
123 | 测试代码存放在它们自己的文件夹中。子文件夹用于区分不同类型的测试,并允许单独运行它们。我们可以将共享的测试
124 | 夹具(_conftest.py_)放在主测试文件夹中,如果需要,还可以嵌套更具体的测试夹具。同时,这也是存放 _pytest.ini_ 的地方。
125 |
126 |
127 |
128 | TIP: The https://oreil.ly/QVb9Q[pytest docs] are really good on test layout and importability.
129 | https://oreil.ly/QVb9Q[pytest 文档] 在测试布局和可导入性方面非常出色。
130 |
131 |
132 | Let's look at a few of these files and concepts in more detail.
133 | 让我们更详细地看一下其中的一些文件和概念。
134 |
135 |
136 |
137 | === Env Vars, 12-Factor, and Config, Inside and Outside Containers
138 | 环境变量、12-Factor原则和配置,在容器内外的使用
139 |
140 | The basic problem we're trying to solve here is that we need different
141 | config settings for the following:
142 | 我们在这里试图解决的基本问题是,对于以下情况,我们需要不同的配置设置:
143 |
144 | - Running code or tests directly from your own dev machine, perhaps
145 | talking to mapped ports from Docker containers
146 | 直接从你自己的开发机器运行代码或测试,可能需要与从 Docker 容器映射的端口进行通信。
147 |
148 | - Running on the containers themselves, with "real" ports and hostnames
149 | 在容器本身上运行,使用“真实”的端口和主机名。
150 |
151 | - Different container environments (dev, staging, prod, and so on)
152 | 不同的容器环境(开发、测试、生产等)。
153 |
154 | Configuration through environment variables as suggested by the
155 | https://12factor.net/config[12-factor manifesto] will solve this problem,
156 | but concretely, how do we implement it in our code and our containers?
157 |
158 | 通过环境变量进行配置(正如 https://12factor.net/config[12-factor 宣言] 所建议的)可以解决这一问题,
159 | 但具体来说,我们如何在代码和容器中实现它呢?
160 |
161 |
162 | === Config.py
163 |
164 | Whenever our application code needs access to some config, it's going to
165 | get it from a file called __config.py__. Here are a couple of examples from our
166 | app:
167 |
168 | 每当我们的应用程序代码需要访问某些配置时,它将从一个名为 __config.py__ 的文件中获取。以下是我们应用程序中的一些示例:
169 |
170 | [[config_dot_py]]
171 | .Sample config functions (src/allocation/config.py)(示例配置函数)
172 | ====
173 | [source,python]
174 | ----
175 | import os
176 |
177 |
178 | def get_postgres_uri(): #<1>
179 | host = os.environ.get("DB_HOST", "localhost") #<2>
180 | port = 54321 if host == "localhost" else 5432
181 | password = os.environ.get("DB_PASSWORD", "abc123")
182 | user, db_name = "allocation", "allocation"
183 | return f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
184 |
185 |
186 | def get_api_url():
187 | host = os.environ.get("API_HOST", "localhost")
188 | port = 5005 if host == "localhost" else 80
189 | return f"http://{host}:{port}"
190 | ----
191 | ====
192 |
193 | <1> We use functions for getting the current config, rather than constants
194 | available at import time, because that allows client code to modify
195 | `os.environ` if it needs to.
196 | 我们使用函数来获取当前配置,而不是在导入时直接使用常量,因为这样可以让客户端代码在需要时修改 `os.environ`。
197 |
198 | <2> _config.py_ also defines some default settings, designed to work when
199 | running the code from the developer's local machine.footnote:[
200 | This gives us a local development setup that "just works" (as much as possible).
201 | You may prefer to fail hard on missing environment variables instead, particularly
202 | if any of the defaults would be insecure in production.]
203 | _config.py_ 还定义了一些默认设置,这些设置旨在支持从开发者的本地机器运行代码时使用。脚注:
204 | 这为我们提供了一个尽可能“开箱即用”的本地开发环境。但你可能更倾向于在缺失环境变量时直接失败,特别是如果任何默认值在生产环境中可能不够安全的话。
205 |
206 | An elegant Python package called
207 | https://github.com/hynek/environ-config[_environ-config_] is worth looking
208 | at if you get tired of hand-rolling your own environment-based config functions.
209 |
210 | 如果你厌倦了手动编写基于环境的配置函数,可以看看一个优雅的 _Python_ 包:https://github.com/hynek/environ-config[_environ-config_]。
211 |
212 | TIP: Don't let this config module become a dumping ground that is full of things only vaguely related to config and that is then imported all over the place.
213 | Keep things immutable and modify them only via environment variables.
214 | If you decide to use a <>,
215 | you can make it the only place (other than tests) that config is imported to.
216 | 不要让这个配置模块变成一个四处堆满仅与配置稍有关系的东西的垃圾场,并且被到处导入。请保持配置的不可变性,仅通过环境变量对其进行修改。
217 | 如果你决定使用一个 <>,可以让它成为唯一(除了测试之外)导入配置的地方。
218 |
219 | === Docker-Compose and Containers Config
220 | Docker-Compose 和容器配置
221 |
222 | We use a lightweight Docker container orchestration tool called _docker-compose_.
223 | It's main configuration is via a YAML file (sigh):footnote:[Harry is a bit YAML-weary.
224 | It's _everywhere_, and yet he can never remember the syntax or how it's supposed
225 | to indent.]
226 |
227 | 我们使用了一种轻量级的 Docker 容器编排工具,称为 _docker-compose_。它的主要配置是通过一个 YAML 文件完成的(唉):脚注:
228 | Harry 对 YAML 有些厌倦了。它无处不在,但他总是记不住它的语法或正确的缩进方式。
229 |
230 |
231 | [[docker_compose]]
232 | .docker-compose config file (docker-compose.yml)(docker-compose 配置文件)
233 | ====
234 | [source,yaml]
235 | ----
236 | version: "3"
237 | services:
238 |
239 | app: #<1>
240 | build:
241 | context: .
242 | dockerfile: Dockerfile
243 | depends_on:
244 | - postgres
245 | environment: #<3>
246 | - DB_HOST=postgres <4>
247 | - DB_PASSWORD=abc123
248 | - API_HOST=app
249 | - PYTHONDONTWRITEBYTECODE=1 #<5>
250 | volumes: #<6>
251 | - ./src:/src
252 | - ./tests:/tests
253 | ports:
254 | - "5005:80" <7>
255 |
256 |
257 | postgres:
258 | image: postgres:9.6 #<2>
259 | environment:
260 | - POSTGRES_USER=allocation
261 | - POSTGRES_PASSWORD=abc123
262 | ports:
263 | - "54321:5432"
264 | ----
265 | ====
266 |
267 | <1> In the _docker-compose_ file, we define the different _services_
268 | (containers) that we need for our app. Usually one main image
269 | contains all our code, and we can use it to run our API, our tests,
270 | or any other service that needs access to the domain model.
271 | 在 _docker-compose_ 文件中,我们定义了应用程序所需的不同 _服务_(容器)。通常,一个主要镜像包含我们所有的代码,
272 | 我们可以用它来运行 API、测试或任何其他需要访问领域模型的服务。
273 |
274 | <2> You'll probably have other infrastructure services, including a database.
275 | In production you might not use containers for this; you might have a cloud
276 | provider instead, but _docker-compose_ gives us a way of producing a
277 | similar service for dev or CI.
278 | 你可能还会有其他基础设施服务,包括数据库。在生产环境中,你可能不会使用容器来运行这些服务,而是可能依赖云供应商,
279 | 但 _docker-compose_ 为我们提供了一种方式,可以在开发或持续集成(CI)环境中生成类似的服务。
280 |
281 | <3> The `environment` stanza lets you set the environment variables for your
282 | containers, the hostnames and ports as seen from inside the Docker cluster.
283 | If you have enough containers that information starts to be duplicated in
284 | these sections, you can use `environment_file` instead. We usually call
285 | ours _container.env_.
286 | `environment` 部分允许你为容器设置环境变量,以及在 Docker 集群内部看到的主机名和端口。如果你的容器数量足够多,
287 | 导致这些信息在这些部分中开始被重复使用,那么可以改用 `environment_file`。我们通常将其命名为 _container.env_。
288 |
289 | <4> Inside a cluster, _docker-compose_ sets up networking such that containers are
290 | available to each other via hostnames named after their service name.
291 | 在集群内部,_docker-compose_ 设置了网络,使得容器可以通过以其服务名称命名的主机名彼此访问。
292 |
293 | <5> Pro tip: if you're mounting volumes to share source folders between your
294 | local dev machine and the container, the `PYTHONDONTWRITEBYTECODE` environment variable
295 | tells Python to not write _.pyc_ files, and that will save you from
296 | having millions of root-owned files sprinkled all over your local filesystem,
297 | being all annoying to delete and causing weird Python compiler errors besides.
298 | 专业提示:如果你正在挂载卷以在本地开发机器与容器之间共享源文件夹,可以设置 `PYTHONDONTWRITEBYTECODE` 环境变量,
299 | 告诉 _Python_ 不要生成 _.pyc_ 文件。这将帮助你避免在本地文件系统中散布大量由 root 拥有的文件,这些文件不仅令人烦恼难以删除,
300 | 还可能导致奇怪的 _Python_ 编译错误。
301 |
302 | <6> Mounting our source and test code as `volumes` means we don't need to rebuild
303 | our containers every time we make a code change.
304 | 将我们的源代码和测试代码挂载为 `volumes` 意味着每次更改代码时,我们不需要重新构建容器。
305 |
306 | <7> The `ports` section allows us to expose the ports from inside the containers
307 | to the outside worldfootnote:[On a CI server, you may not be able to expose
308 | arbitrary ports reliably, but it's only a convenience for local dev. You
309 | can find ways of making these port mappings optional (e.g., with
310 | _docker-compose.override.yml_).]—these correspond to the default ports we set
311 | in _config.py_.
312 | `ports` 部分允许我们将容器内部的端口暴露给外部世界。脚注:
313 | 在 CI 服务器上,你可能无法可靠地暴露任意端口,但这仅是为了本地开发的便利。你可以找到方法使这些端口映射成为可选的
314 | (例如,使用 _docker-compose.override.yml_)。这些端口与我们在 _config.py_ 中设置的默认端口相对应。
315 |
316 | NOTE: Inside Docker, other containers are available through hostnames named after
317 | their service name. Outside Docker, they are available on `localhost`, at the
318 | port defined in the `ports` section.
319 | 在 Docker 内部,可以通过以服务名称命名的主机名访问其他容器。在 Docker 外部,可以通过 `localhost` 访问它们,端口由 `ports` 部分定义。
320 |
321 |
322 | === Installing Your Source as a Package
323 | 将源代码安装为一个包
324 |
325 | All our application code (everything except tests, really) lives inside an
326 | _src_ folder:
327 |
328 | 我们所有的应用程序代码(实际上除了测试以外的所有内容)都放在一个 _src_ 文件夹中:
329 |
330 | [[src_folder_tree]]
331 | .The src folder(src 文件夹)
332 | ====
333 | [source,text]
334 | [role="skip"]
335 | ----
336 | ├── src
337 | │ ├── allocation #<1>
338 | │ │ ├── config.py
339 | │ │ └── ...
340 | │ └── setup.py <2>
341 | ----
342 | ====
343 |
344 | <1> Subfolders define top-level module names. You can have multiple if you like.
345 | 子文件夹定义了顶级模块名称。如果你需要,可以有多个。
346 |
347 | <2> And _setup.py_ is the file you need to make it pip-installable, shown next.
348 | 而 _setup.py_ 是让其支持通过 pip 安装所需的文件,如下所示。
349 |
350 | [[setup_dot_py]]
351 | .pip-installable modules in three lines (src/setup.py)(用三行代码实现可通过 pip 安装的模块)
352 | ====
353 | [source,python]
354 | ----
355 | from setuptools import setup
356 |
357 | setup(
358 | name="allocation", version="0.1", packages=["allocation"],
359 | )
360 | ----
361 | ====
362 |
363 | That's all you need. `packages=` specifies the names of subfolders that you
364 | want to install as top-level modules. The `name` entry is just cosmetic, but
365 | it's required. For a package that's never actually going to hit PyPI, it'll
366 | do fine.footnote:[For more _setup.py_ tips, see
367 | https://oreil.ly/KMWDz[this article on packaging] by Hynek.]
368 |
369 | 这就是你所需的一切。`packages=` 指定你希望安装为顶级模块的子文件夹名称。`name` 条目只是一个装饰性选项,但它是必需的。
370 | 对于一个永远不会真正发布到 PyPI 的包来说,这样已经足够了。脚注:
371 | 有关更多 _setup.py_ 技巧,请参阅 Hynek 的这篇文章: https://oreil.ly/KMWDz[关于打包的文章]。
372 |
373 |
374 | === Dockerfile
375 |
376 | Dockerfiles are going to be very project-specific, but here are a few key stages
377 | you'll expect to see:
378 |
379 | Dockerfile 将会非常依赖具体项目,但以下是你可能会看到的一些关键阶段:
380 |
381 | [[dockerfile]]
382 | .Our Dockerfile (Dockerfile)(我们的 Dockerfile)
383 | ====
384 | [source,dockerfile]
385 | ----
386 | FROM python:3.9-slim-buster
387 |
388 | <1>
389 | # RUN apt install gcc libpq (no longer needed bc we use psycopg2-binary)
390 |
391 | <2>
392 | COPY requirements.txt /tmp/
393 | RUN pip install -r /tmp/requirements.txt
394 |
395 | <3>
396 | RUN mkdir -p /src
397 | COPY src/ /src/
398 | RUN pip install -e /src
399 | COPY tests/ /tests/
400 |
401 | <4>
402 | WORKDIR /src
403 | ENV FLASK_APP=allocation/entrypoints/flask_app.py FLASK_DEBUG=1 PYTHONUNBUFFERED=1
404 | CMD flask run --host=0.0.0.0 --port=80
405 | ----
406 | ====
407 |
408 | <1> Installing system-level dependencies
409 | 安装系统级依赖项
410 | <2> Installing our Python dependencies (you may want to split out your dev from
411 | prod dependencies; we haven't here, for simplicity)
412 | 安装我们的 _Python_ 依赖项(你可能希望将开发依赖和生产依赖分开;为了简单起见,我们在这里没有这样做)
413 | <3> Copying and installing our source
414 | 复制并安装我们的源代码
415 | <4> Optionally configuring a default startup command (you'll probably override
416 | this a lot from the command line)
417 | 可选地配置一个默认的启动命令(你可能会经常从命令行覆盖它)。
418 |
419 | TIP: One thing to note is that we install things in the order of how frequently they
420 | are likely to change. This allows us to maximize Docker build cache reuse. I
421 | can't tell you how much pain and frustration underlies this lesson. For this
422 | and many more Python Dockerfile improvement tips, check out
423 | https://pythonspeed.com/docker["Production-Ready Docker Packaging"].
424 | 需要注意的一点是,我们按照更改频率的顺序安装内容。这样可以最大化 Docker 构建缓存的重用。我无法形容这个教训背后蕴含了多少痛苦和挫折。
425 | 有关这一点以及更多关于改进 _Python_ Dockerfile 的技巧,请查看: https://pythonspeed.com/docker["生产就绪的 Docker 打包"]。
426 |
427 | === Tests
428 | 测试
429 |
430 | ((("testing", "tests folder tree")))
431 | Our tests are kept alongside everything else, as shown here:
432 |
433 | 我们的测试代码与其他内容一起存放,如下所示:
434 |
435 | [[tests_folder]]
436 | .Tests folder tree(测试文件夹结构树)
437 | ====
438 | [source,text]
439 | [role="tree"]
440 | ----
441 | └── tests
442 | ├── conftest.py
443 | ├── e2e
444 | │ └── test_api.py
445 | ├── integration
446 | │ ├── test_orm.py
447 | │ └── test_repository.py
448 | ├── pytest.ini
449 | └── unit
450 | ├── test_allocate.py
451 | ├── test_batches.py
452 | └── test_services.py
453 | ----
454 | ====
455 |
456 | Nothing particularly clever here, just some separation of different test types
457 | that you're likely to want to run separately, and some files for common fixtures,
458 | config, and so on.
459 |
460 | 这里并没有什么特别的巧妙之处,只是对可能需要单独运行的不同类型测试进行了分类,并提供了一些文件用于共享的夹具、配置等。
461 |
462 | There's no _src_ folder or _setup.py_ in the test folders because we usually
463 | haven't needed to make tests pip-installable, but if you have difficulties with
464 | import paths, you might find it helps.
465 |
466 | 测试文件夹中没有 _src_ 文件夹或 _setup.py_,因为我们通常不需要让测试代码支持通过 pip 安装。
467 | 但如果你在导入路径方面遇到困难,这可能会有所帮助。
468 |
469 |
470 | === Wrap-Up
471 | 总结
472 |
473 | These are our basic building blocks:
474 |
475 | 以下是我们的基本构建块:
476 |
477 | * Source code in an _src_ folder, pip-installable using _setup.py_
478 | 源代码存放在 _src_ 文件夹中,可通过 _setup.py_ 进行 pip 安装。
479 | * Some Docker config for spinning up a local cluster that mirrors production as far as possible
480 | 一些 Docker 配置,用于启动尽可能接近生产环境的本地集群。
481 | * Configuration via environment variables, centralized in a Python file called _config.py_, with defaults allowing things to run _outside_ containers
482 | 通过环境变量进行配置,集中在一个名为 _config.py_ 的 Python 文件中,并带有默认值,允许在容器 _外部_ 运行代码。
483 | * A Makefile for useful command-line, um, commands
484 | 一个用于便捷命令行操作的 Makefile
485 |
486 | ((("projects", "template project structure", startref="ix_prjstrct")))
487 | We doubt that anyone will end up with _exactly_ the same solutions we did, but we hope you
488 | find some inspiration here.
489 |
490 | 我们怀疑是否会有人最终采用与我们 _完全_ 相同的解决方案,但我们希望你能从中获得一些灵感。
491 |
--------------------------------------------------------------------------------
/atlas.json:
--------------------------------------------------------------------------------
1 | {
2 | "branch": "master",
3 | "files": [
4 | "cover.html",
5 | "titlepage.html",
6 | "copyright.html",
7 | "toc.html",
8 | "preface.asciidoc",
9 | "introduction.asciidoc",
10 | "part1.asciidoc",
11 | "chapter_01_domain_model.asciidoc",
12 | "chapter_02_repository.asciidoc",
13 | "chapter_03_abstractions.asciidoc",
14 | "chapter_04_service_layer.asciidoc",
15 | "chapter_05_high_gear_low_gear.asciidoc",
16 | "chapter_06_uow.asciidoc",
17 | "chapter_07_aggregate.asciidoc",
18 | "part2.asciidoc",
19 | "chapter_08_events_and_message_bus.asciidoc",
20 | "chapter_09_all_messagebus.asciidoc",
21 | "chapter_10_commands.asciidoc",
22 | "chapter_11_external_events.asciidoc",
23 | "chapter_12_cqrs.asciidoc",
24 | "chapter_13_dependency_injection.asciidoc",
25 | "epilogue_1_how_to_get_there_from_here.asciidoc",
26 | "appendix_ds1_table.asciidoc",
27 | "appendix_project_structure.asciidoc",
28 | "appendix_csvs.asciidoc",
29 | "appendix_django.asciidoc",
30 | "appendix_validation.asciidoc",
31 | "ix.html",
32 | "author_bio.html",
33 | "colo.html"
34 | ],
35 | "formats": {
36 | "pdf": {
37 | "version": "web",
38 | "color_count": "4",
39 | "index": true,
40 | "toc": true,
41 | "syntaxhighlighting": true,
42 | "show_comments": false,
43 | "trim_size": "7inx9.1875in",
44 | "antennahouse_version": "AHFormatterV62_64-MR4"
45 | },
46 | "epub": {
47 | "index": true,
48 | "toc": true,
49 | "epubcheck": true,
50 | "syntaxhighlighting": true,
51 | "show_comments": false,
52 | "downsample_images": false,
53 | "mathmlreplacement": false
54 | },
55 | "mobi": {
56 | "index": true,
57 | "toc": true,
58 | "syntaxhighlighting": true,
59 | "show_comments": false,
60 | "downsample_images": false
61 | },
62 | "html": {
63 | "index": true,
64 | "toc": true,
65 | "syntaxhighlighting": true,
66 | "show_comments": false,
67 | "consolidated": false
68 | }
69 | },
70 | "theme": "oreillymedia/animal_theme_sass",
71 | "title": "Architecture Patterns with Python",
72 | "print_isbn13": "9781492052203",
73 | "lang": "en",
74 | "accent_color": "",
75 | "templating": false
76 | }
--------------------------------------------------------------------------------
/author_bio.html:
--------------------------------------------------------------------------------
1 |
2 |
About the Authors
3 |
Harry Percival spent a few years being deeply unhappy as a management consultant. Soon he rediscovered his true geek nature and was lucky enough to fall in with a bunch of XP fanatics, working on pioneering the sadly defunct Resolver One spreadsheet. He worked at PythonAnywhere LLP, spreading the gospel of TDD worldwide at talks, workshops, and conferences. He is now with MADE.com.
4 |
5 |
Bob Gregory is a UK-based software architect with MADE.com. He has been building event-driven systems with domain-driven design for more than a decade.
The animal on the cover of Architecture Patterns with Python is a Burmese python (Python bivitattus). As you might expect, the Burmese python is native to Southeast Asia. Today it lives in jungles and marshes in South Asia, Myanmar, China, and Indonesia; it’s also invasive in Florida’s Everglades.
5 |
6 |
Burmese pythons are one of the world’s largest species of snakes. These nocturnal, carnivorous constrictors can grow to 23 feet and 200 pounds. Females are larger than males. They can lay up to a hundred eggs in one clutch. In the wild, Burmese pythons live an average of 20 to 25 years.
7 |
8 |
The markings on a Burmese python begin with an arrow-shaped spot of light brown on top of the head and continue along the body in rectangles that stand out against its otherwise tan scales. Before they reach their full size, which takes two to three years, Burmese pythons live in trees hunting small mammals and birds. They also swim for long stretches of time—going up to 30 minutes without air.
9 |
10 |
Because of habitat destruction, the Burmese python has a conservation status of Vulnerable. Many of the animals on O’Reilly’s covers are endangered; all of them are important to the world.
11 |
12 |
The color illustration is by Jose Marzan, based on a black-and-white engraving from Encyclopedie D'Histoire Naturelle. The cover fonts are URW Typewriter and Guardian Sans. The text font is Adobe Minion Pro; the heading font is Adobe Myriad Condensed; and the code font is Dalton Maag's Ubuntu Mono.
Published by O'Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.
11 |
12 |
O'Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://oreilly.com). For more information, contact our corporate/institutional sales department: 800-998-9938 or corporate@oreilly.com.
The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Architecture Patterns with Python, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc.
43 |
44 |
The views expressed in this work are those of the authors and do not represent the publisher's views. While the publisher and the authors have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the authors disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights.
48 |
49 |
50 |
51 |
978-1-492-05220-3
52 |
53 |
[LSI]
54 |
55 |
56 |
--------------------------------------------------------------------------------
/cover.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/fix-branches.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import subprocess
4 | from pathlib import Path
5 | from chapters import CHAPTERS
6 |
7 | for chapter in CHAPTERS:
8 | subprocess.run(
9 | ['git', '-c', 'pager.show=false', 'show', '-s', '--oneline', f':/{chapter}_ends'],
10 | cwd=Path(__file__).parent / 'code'
11 | )
12 | subprocess.run(
13 | ['git', 'branch', '-f', chapter, f':/{chapter}_ends'],
14 | cwd=Path(__file__).parent / 'code'
15 | )
16 | subprocess.run(
17 | ['git', 'diff', f'origin/{chapter}', chapter],
18 | cwd=Path(__file__).parent / 'code'
19 | )
20 |
--------------------------------------------------------------------------------
/images/C4.puml:
--------------------------------------------------------------------------------
1 | ' C4-PlantUML, version 1.0.0
2 | ' https://github.com/RicardoNiepel/C4-PlantUML
3 |
4 | ' Colors
5 | ' ##################################
6 |
7 | !define ELEMENT_FONT_COLOR #FFFFFF
8 |
9 | ' Styling
10 | ' ##################################
11 |
12 | !define TECHN_FONT_SIZE 18
13 |
14 | skinparam roundCorner 20
15 | skinparam Padding 2
16 | skinparam wrapWidth 200
17 | skinparam default {
18 | FontName Guardian Sans Cond Regular
19 | FontSize 18
20 | }
21 |
22 | skinparam defaultTextAlignment center
23 |
24 | skinparam wrapWidth 200
25 | skinparam maxMessageSize 150
26 |
27 | skinparam rectangle {
28 | StereotypeFontSize 18
29 | shadowing false
30 | }
31 |
32 | skinparam database {
33 | StereotypeFontSize 18
34 | shadowing false
35 | }
36 |
37 | skinparam Arrow {
38 | Color #666666
39 | FontColor #666666
40 | FontSize 18
41 | }
42 |
43 | skinparam rectangle<> {
44 | Shadowing false
45 | StereotypeFontSize 0
46 | FontColor #444444
47 | BorderColor #444444
48 | BorderStyle dashed
49 | }
50 |
51 | ' Layout
52 | ' ##################################
53 |
54 | !definelong LAYOUT_AS_SKETCH
55 | skinparam backgroundColor #EEEBDC
56 | skinparam handwritten true
57 | skinparam defaultFontName "Comic Sans MS"
58 | center footer Warning: Created for discussion, needs to be validated
59 | !enddefinelong
60 |
61 | !define LAYOUT_TOP_DOWN top to bottom direction
62 | !define LAYOUT_LEFT_RIGHT left to right direction
63 |
64 | ' Boundaries
65 | ' ##################################
66 |
67 | !define Boundary(e_alias, e_label) rectangle "==e_label" <> as e_alias
68 | !define Boundary(e_alias, e_label, e_type) rectangle "==e_label\n[e_type]" <> as e_alias
69 |
70 | ' Relationship
71 | ' ##################################
72 |
73 | !define Rel_(e_alias1, e_alias2, e_label, e_direction="") e_alias1 e_direction e_alias2 : "===e_label"
74 | !define Rel_(e_alias1, e_alias2, e_label, e_techn, e_direction="") e_alias1 e_direction e_alias2 : "===e_label\n//[e_techn]//"
75 |
76 | !define Rel(e_from,e_to, e_label) Rel_(e_from,e_to, e_label, "-->")
77 | !define Rel(e_from,e_to, e_label, e_techn) Rel_(e_from,e_to, e_label, e_techn, "-->")
78 |
79 | !define Rel_Back(e_to, e_from, e_label) Rel_(e_to, e_from, e_label, "<--")
80 | !define Rel_Back(e_to, e_from, e_label, e_techn) Rel_(e_to, e_from, e_label, e_techn, "<--")
81 |
82 | !define Rel_Neighbor(e_from,e_to, e_label) Rel_(e_from,e_to, e_label, "->")
83 | !define Rel_Neighbor(e_from,e_to, e_label, e_techn) Rel_(e_from,e_to, e_label, e_techn, "->")
84 |
85 | !define Rel_Back_Neighbor(e_to, e_from, e_label) Rel_(e_to, e_from, e_label, "<-")
86 | !define Rel_Back_Neighbor(e_to, e_from, e_label, e_techn) Rel_(e_to, e_from, e_label, e_techn, "<-")
87 |
88 | !define Rel_D(e_from,e_to, e_label) Rel_(e_from,e_to, e_label, "-DOWN->")
89 | !define Rel_D(e_from,e_to, e_label, e_techn) Rel_(e_from,e_to, e_label, e_techn, "-DOWN->")
90 | !define Rel_Down(e_from,e_to, e_label) Rel_D(e_from,e_to, e_label)
91 | !define Rel_Down(e_from,e_to, e_label, e_techn) Rel_D(e_from,e_to, e_label, e_techn)
92 |
93 | !define Rel_U(e_from,e_to, e_label) Rel_(e_from,e_to, e_label, "-UP->")
94 | !define Rel_U(e_from,e_to, e_label, e_techn) Rel_(e_from,e_to, e_label, e_techn, "-UP->")
95 | !define Rel_Up(e_from,e_to, e_label) Rel_U(e_from,e_to, e_label)
96 | !define Rel_Up(e_from,e_to, e_label, e_techn) Rel_U(e_from,e_to, e_label, e_techn)
97 |
98 | !define Rel_L(e_from,e_to, e_label) Rel_(e_from,e_to, e_label, "-LEFT->")
99 | !define Rel_L(e_from,e_to, e_label, e_techn) Rel_(e_from,e_to, e_label, e_techn, "-LEFT->")
100 | !define Rel_Left(e_from,e_to, e_label) Rel_L(e_from,e_to, e_label)
101 | !define Rel_Left(e_from,e_to, e_label, e_techn) Rel_L(e_from,e_to, e_label, e_techn)
102 |
103 | !define Rel_R(e_from,e_to, e_label) Rel_(e_from,e_to, e_label, "-RIGHT->")
104 | !define Rel_R(e_from,e_to, e_label, e_techn) Rel_(e_from,e_to, e_label, e_techn, "-RIGHT->")
105 | !define Rel_Right(e_from,e_to, e_label) Rel_R(e_from,e_to, e_label)
106 | !define Rel_Right(e_from,e_to, e_label, e_techn) Rel_R(e_from,e_to, e_label, e_techn)
107 |
108 | ' Layout Helpers
109 | ' ##################################
110 |
111 | !define Lay_D(e_from, e_to) e_from -[hidden]D- e_to
112 | !define Lay_U(e_from, e_to) e_from -[hidden]U- e_to
113 | !define Lay_R(e_from, e_to) e_from -[hidden]R- e_to
114 | !define Lay_L(e_from, e_to) e_from -[hidden]L- e_to
115 |
--------------------------------------------------------------------------------
/images/C4_Component.puml:
--------------------------------------------------------------------------------
1 | ' !includeurl https://raw.githubusercontent.com/RicardoNiepel/C4-PlantUML/master/C4_Container.puml
2 | ' uncomment the following line and comment the first to use locally
3 | !include C4_Container.puml
4 |
5 | ' Scope: A single container.
6 | ' Primary elements: Components within the container in scope.
7 | ' Supporting elements: Containers (within the software system in scope) plus people and software systems directly connected to the components.
8 | ' Intended audience: Software architects and developers.
9 |
10 | ' Colors
11 | ' ##################################
12 |
13 | !define COMPONENT_BG_COLOR #85BBF0
14 |
15 | ' Styling
16 | ' ##################################
17 |
18 | skinparam rectangle<> {
19 | StereotypeFontColor ELEMENT_FONT_COLOR
20 | FontColor #000000
21 | BackgroundColor COMPONENT_BG_COLOR
22 | BorderColor #78A8D8
23 | }
24 |
25 | skinparam database<> {
26 | StereotypeFontColor ELEMENT_FONT_COLOR
27 | FontColor #000000
28 | BackgroundColor COMPONENT_BG_COLOR
29 | BorderColor #78A8D8
30 | }
31 |
32 | ' Layout
33 | ' ##################################
34 |
35 | !definelong LAYOUT_WITH_LEGEND
36 | hide stereotype
37 | legend right
38 | |= |= Type |
39 | | | person |
40 | | | external person |
41 | | | system |
42 | | | external system |
43 | | | container |
44 | | | component |
45 | endlegend
46 | !enddefinelong
47 |
48 | ' Elements
49 | ' ##################################
50 |
51 | !define Component(e_alias, e_label, e_techn) rectangle "==e_label\n//[e_techn]//" <> as e_alias
52 | !define Component(e_alias, e_label, e_techn, e_descr) rectangle "==e_label\n//[e_techn]//\n\n e_descr" <> as e_alias
53 |
54 | !define ComponentDb(e_alias, e_label, e_techn) database "==e_label\n//[e_techn]//" <> as e_alias
55 | !define ComponentDb(e_alias, e_label, e_techn, e_descr) database "==e_label\n//[e_techn]//\n\n e_descr" <> as e_alias
56 |
--------------------------------------------------------------------------------
/images/C4_Container.puml:
--------------------------------------------------------------------------------
1 | ' !includeurl https://raw.githubusercontent.com/RicardoNiepel/C4-PlantUML/master/C4_Context.puml
2 | ' uncomment the following line and comment the first to use locally
3 | !include C4_Context.puml
4 |
5 | ' Scope: A single software system.
6 | ' Primary elements: Containers within the software system in scope.
7 | ' Supporting elements: People and software systems directly connected to the containers.
8 | ' Intended audience: Technical people inside and outside of the software development team; including software architects, developers and operations/support staff.
9 |
10 | ' Colors
11 | ' ##################################
12 |
13 | !define CONTAINER_BG_COLOR #438DD5
14 |
15 | ' Styling
16 | ' ##################################
17 |
18 | skinparam rectangle<> {
19 | StereotypeFontColor ELEMENT_FONT_COLOR
20 | FontColor ELEMENT_FONT_COLOR
21 | BackgroundColor CONTAINER_BG_COLOR
22 | BorderColor #3C7FC0
23 | }
24 |
25 | skinparam database<> {
26 | StereotypeFontColor ELEMENT_FONT_COLOR
27 | FontColor ELEMENT_FONT_COLOR
28 | BackgroundColor CONTAINER_BG_COLOR
29 | BorderColor #3C7FC0
30 | }
31 |
32 | ' Layout
33 | ' ##################################
34 |
35 | !definelong LAYOUT_WITH_LEGEND
36 | hide stereotype
37 | legend right
38 | |= |= Type |
39 | | | person |
40 | | | external person |
41 | | | system |
42 | | | external system |
43 | | | container |
44 | endlegend
45 | !enddefinelong
46 |
47 | ' Elements
48 | ' ##################################
49 |
50 | !define Container(e_alias, e_label, e_techn) rectangle "==e_label\n//[e_techn]//" <> as e_alias
51 | !define Container(e_alias, e_label, e_techn, e_descr) rectangle "==e_label\n//[e_techn]//\n\n e_descr" <> as e_alias
52 |
53 | !define ContainerDb(e_alias, e_label, e_techn) database "==e_label\n//[e_techn]//" <> as e_alias
54 | !define ContainerDb(e_alias, e_label, e_techn, e_descr) database "==e_label\n//[e_techn]//\n\n e_descr" <> as e_alias
55 |
56 | ' Boundaries
57 | ' ##################################
58 |
59 | !define Container_Boundary(e_alias, e_label) Boundary(e_alias, e_label, "Container")
--------------------------------------------------------------------------------
/images/C4_Context.puml:
--------------------------------------------------------------------------------
1 | ' !includeurl https://raw.githubusercontent.com/RicardoNiepel/C4-PlantUML/master/C4.puml
2 | ' uncomment the following line and comment the first to use locally
3 | !include C4.puml
4 |
5 | ' Scope: A single software system.
6 | ' Primary elements: The software system in scope.
7 | ' Supporting elements: People and software systems directly connected to the software system in scope.
8 | ' Intended audience: Everybody, both technical and non-technical people, inside and outside of the software development team.
9 |
10 | ' Colors
11 | ' ##################################
12 |
13 | !define PERSON_BG_COLOR #08427B
14 | !define EXTERNAL_PERSON_BG_COLOR #686868
15 | !define SYSTEM_BG_COLOR #1168BD
16 | !define EXTERNAL_SYSTEM_BG_COLOR #999999
17 |
18 | ' Styling
19 | ' ##################################
20 |
21 | skinparam rectangle<> {
22 | StereotypeFontColor ELEMENT_FONT_COLOR
23 | FontColor ELEMENT_FONT_COLOR
24 | BackgroundColor PERSON_BG_COLOR
25 | BorderColor #073B6F
26 | }
27 |
28 | skinparam rectangle<> {
29 | StereotypeFontColor ELEMENT_FONT_COLOR
30 | FontColor ELEMENT_FONT_COLOR
31 | BackgroundColor EXTERNAL_PERSON_BG_COLOR
32 | BorderColor #8A8A8A
33 | }
34 |
35 | skinparam rectangle<> {
36 | StereotypeFontColor ELEMENT_FONT_COLOR
37 | FontColor ELEMENT_FONT_COLOR
38 | BackgroundColor SYSTEM_BG_COLOR
39 | BorderColor #3C7FC0
40 | }
41 |
42 | skinparam rectangle<> {
43 | StereotypeFontColor ELEMENT_FONT_COLOR
44 | FontColor ELEMENT_FONT_COLOR
45 | BackgroundColor EXTERNAL_SYSTEM_BG_COLOR
46 | BorderColor #8A8A8A
47 | }
48 |
49 | skinparam database<> {
50 | StereotypeFontColor ELEMENT_FONT_COLOR
51 | FontColor ELEMENT_FONT_COLOR
52 | BackgroundColor SYSTEM_BG_COLOR
53 | BorderColor #3C7FC0
54 | }
55 |
56 | skinparam database<> {
57 | StereotypeFontColor ELEMENT_FONT_COLOR
58 | FontColor ELEMENT_FONT_COLOR
59 | BackgroundColor EXTERNAL_SYSTEM_BG_COLOR
60 | BorderColor #8A8A8A
61 | }
62 |
63 | ' Layout
64 | ' ##################################
65 |
66 | !definelong LAYOUT_WITH_LEGEND
67 | hide stereotype
68 | legend right
69 | |= |= Type |
70 | | | person |
71 | | | external person |
72 | | | system |
73 | | | external system |
74 | endlegend
75 | !enddefinelong
76 |
77 | ' Elements
78 | ' ##################################
79 |
80 | !define Person(e_alias, e_label) rectangle "==e_label" <> as e_alias
81 | !define Person(e_alias, e_label, e_descr) rectangle "==e_label\n\n e_descr" <> as e_alias
82 |
83 | !define Person_Ext(e_alias, e_label) rectangle "==e_label" <> as e_alias
84 | !define Person_Ext(e_alias, e_label, e_descr) rectangle "==e_label\n\n e_descr" <> as e_alias
85 |
86 | !define System(e_alias, e_label) rectangle "==e_label" <> as e_alias
87 | !define System(e_alias, e_label, e_descr) rectangle "==e_label\n\n e_descr" <> as e_alias
88 |
89 | !define System_Ext(e_alias, e_label) rectangle "==e_label" <> as e_alias
90 | !define System_Ext(e_alias, e_label, e_descr) rectangle "==e_label\n\n e_descr" <> as e_alias
91 |
92 | !define SystemDb(e_alias, e_label) database "==e_label" <> as e_alias
93 | !define SystemDb(e_alias, e_label, e_descr) database "==e_label\n\n e_descr" <> as e_alias
94 |
95 | !define SystemDb_Ext(e_alias, e_label) database "==e_label" <> as e_alias
96 | !define SystemDb_Ext(e_alias, e_label, e_descr) database "==e_label\n\n e_descr" <> as e_alias
97 |
98 | ' Boundaries
99 | ' ##################################
100 |
101 | !define Enterprise_Boundary(e_alias, e_label) Boundary(e_alias, e_label, "Enterprise")
102 | !define System_Boundary(e_alias, e_label) Boundary(e_alias, e_label, "System")
103 |
--------------------------------------------------------------------------------
/images/apwp_0001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0001.png
--------------------------------------------------------------------------------
/images/apwp_0002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0002.png
--------------------------------------------------------------------------------
/images/apwp_0101.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0101.png
--------------------------------------------------------------------------------
/images/apwp_0102.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0102.png
--------------------------------------------------------------------------------
/images/apwp_0103.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0103.png
--------------------------------------------------------------------------------
/images/apwp_0104.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0104.png
--------------------------------------------------------------------------------
/images/apwp_0201.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0201.png
--------------------------------------------------------------------------------
/images/apwp_0202.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0202.png
--------------------------------------------------------------------------------
/images/apwp_0203.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0203.png
--------------------------------------------------------------------------------
/images/apwp_0204.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0204.png
--------------------------------------------------------------------------------
/images/apwp_0205.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0205.png
--------------------------------------------------------------------------------
/images/apwp_0206.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0206.png
--------------------------------------------------------------------------------
/images/apwp_0301.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0301.png
--------------------------------------------------------------------------------
/images/apwp_0302.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0302.png
--------------------------------------------------------------------------------
/images/apwp_0401.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0401.png
--------------------------------------------------------------------------------
/images/apwp_0402.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0402.png
--------------------------------------------------------------------------------
/images/apwp_0403.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0403.png
--------------------------------------------------------------------------------
/images/apwp_0404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0404.png
--------------------------------------------------------------------------------
/images/apwp_0405.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0405.png
--------------------------------------------------------------------------------
/images/apwp_0501.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0501.png
--------------------------------------------------------------------------------
/images/apwp_0601.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0601.png
--------------------------------------------------------------------------------
/images/apwp_0602.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0602.png
--------------------------------------------------------------------------------
/images/apwp_0701.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0701.png
--------------------------------------------------------------------------------
/images/apwp_0702.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0702.png
--------------------------------------------------------------------------------
/images/apwp_0703.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0703.png
--------------------------------------------------------------------------------
/images/apwp_0704.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0704.png
--------------------------------------------------------------------------------
/images/apwp_0705.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0705.png
--------------------------------------------------------------------------------
/images/apwp_0801.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0801.png
--------------------------------------------------------------------------------
/images/apwp_0901.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0901.png
--------------------------------------------------------------------------------
/images/apwp_0902.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0902.png
--------------------------------------------------------------------------------
/images/apwp_0903.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0903.png
--------------------------------------------------------------------------------
/images/apwp_0904.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_0904.png
--------------------------------------------------------------------------------
/images/apwp_1101.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_1101.png
--------------------------------------------------------------------------------
/images/apwp_1102.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_1102.png
--------------------------------------------------------------------------------
/images/apwp_1103.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_1103.png
--------------------------------------------------------------------------------
/images/apwp_1104.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_1104.png
--------------------------------------------------------------------------------
/images/apwp_1105.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_1105.png
--------------------------------------------------------------------------------
/images/apwp_1106.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_1106.png
--------------------------------------------------------------------------------
/images/apwp_1201.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_1201.png
--------------------------------------------------------------------------------
/images/apwp_1202.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_1202.png
--------------------------------------------------------------------------------
/images/apwp_1301.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_1301.png
--------------------------------------------------------------------------------
/images/apwp_1302.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_1302.png
--------------------------------------------------------------------------------
/images/apwp_1303.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_1303.png
--------------------------------------------------------------------------------
/images/apwp_aa01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_aa01.png
--------------------------------------------------------------------------------
/images/apwp_ep01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_ep01.png
--------------------------------------------------------------------------------
/images/apwp_ep02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_ep02.png
--------------------------------------------------------------------------------
/images/apwp_ep03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_ep03.png
--------------------------------------------------------------------------------
/images/apwp_ep04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_ep04.png
--------------------------------------------------------------------------------
/images/apwp_ep05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_ep05.png
--------------------------------------------------------------------------------
/images/apwp_ep06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_ep06.png
--------------------------------------------------------------------------------
/images/apwp_p101.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_p101.png
--------------------------------------------------------------------------------
/images/apwp_p201.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/apwp_p201.png
--------------------------------------------------------------------------------
/images/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/images/cover.png
--------------------------------------------------------------------------------
/introduction.asciidoc:
--------------------------------------------------------------------------------
1 | [[introduction]]
2 | [preface]
3 | == Introduction
4 | 引言
5 |
6 | // TODO (CC): remove "preface" marker from this chapter and check if they renumber correctly
7 | // with this as zero. figures in this chapter should be "Figure 0-1 etc"
8 |
9 | === Why Do Our Designs Go Wrong?
10 | 为什么我们的设计会出问题?
11 |
12 | What comes to mind when you hear the word _chaos?_ Perhaps you think of a noisy
13 | stock exchange, or your kitchen in the morning--everything confused and
14 | jumbled. When you think of the word _order_, perhaps you think of an empty room,
15 | serene and calm. For scientists, though, chaos is characterized by homogeneity
16 | (sameness), and order by complexity (difference).
17 |
18 | 当你听到 _混乱_ 这个词时,会联想到什么?也许你会想到喧闹的股票交易所,或者清晨混乱不堪的厨房——一切都显得迷乱和杂乱。
19 | 而当你想到 _秩序_ 这个词时,也许会联想到一个空荡荡的房间,祥和而平静。然而,对于科学家来说,混乱的特征是同质性(相同性),
20 | 而秩序的特征则是复杂性(差异性)。
21 |
22 | ////
23 | IDEA [SG] Found previous paragraph a bit confusing. It seems to suggest that a
24 | scientist would say that a noisy stock exchange is ordered. I feel like you
25 | want to talk about Entropy but do not want to go down that rabbit hole.
26 | ////
27 |
28 | For example, a well-tended garden is a highly ordered system. Gardeners define
29 | boundaries with paths and fences, and they mark out flower beds or vegetable
30 | patches. Over time, the garden evolves, growing richer and thicker; but without
31 | deliberate effort, the garden will run wild. Weeds and grasses will choke out
32 | other plants, covering over the paths, until eventually every part looks the
33 | same again--wild and unmanaged.
34 |
35 | 例如,一个精心打理的花园是一个高度有序的系统。园丁用小路和篱笆划定边界,并设计出花坛或菜圃。
36 | 随着时间的推移,花园会演变得更加丰富茂盛;但如果没有刻意的维护,花园就会变得杂乱无章。
37 | 杂草和野草会覆盖其他植物,掩盖小路,直到最终每个部分都变得一样——野蛮生长且无人管理。
38 |
39 | Software systems, too, tend toward chaos. When we first start building a new
40 | system, we have grand ideas that our code will be clean and well ordered, but
41 | over time we find that it gathers cruft and edge cases and ends up a confusing
42 | morass of manager classes and util modules. We find that our sensibly layered
43 | architecture has collapsed into itself like an oversoggy trifle. Chaotic
44 | software systems are characterized by a sameness of function: API handlers that
45 | have domain knowledge and send email and perform logging; "business logic"
46 | classes that perform no calculations but do perform I/O; and everything coupled
47 | to everything else so that changing any part of the system becomes fraught with
48 | danger. This is so common that software engineers have their own term for
49 | chaos: the Big Ball of Mud antipattern (<>).
50 |
51 | 软件系统同样也倾向于走向混乱。一开始,当我们构建一个新系统时,我们满怀壮志,认为代码会保持干净且有序。
52 | 然而,随着时间推移,我们发现代码积累了杂乱无章的内容与边缘案例,最终变成了一团混乱的管理类和工具模块的沼泽。
53 | 原本合理分层的架构也塌陷了,如同一盘过于湿软的松糕。混乱的软件系统以功能的同质性为特征:比如,
54 | API处理程序既包含领域知识,又发送电子邮件并执行日志记录;所谓的“业务逻辑”类不进行计算,却执行输入/输出操作;
55 | 每个组件都与其他组件紧密耦合,以至于修改系统的任何部分都会变得充满风险。
56 | 这种情况非常常见,以至于软件工程师用自己的术语来描述这种混乱:*大泥球反模式*(Big Ball of Mud)(<>)。
57 |
58 | [[bbom_image]]
59 | .A real-life dependency diagram (source: https://oreil.ly/dbGTW["Enterprise Dependency: Big Ball of Yarn"] by Alex Papadimoulis)(一个现实场景的依赖关系图)
60 | image::images/apwp_0001.png[]
61 |
62 | TIP: A big ball of mud is the natural state of software in the same way that wilderness
63 | is the natural state of your garden. It takes energy and direction to
64 | prevent the collapse.
65 | 大泥球是软件的自然状态,就像荒野是你花园的自然状态一样。需要付出精力和明确的指导才能防止其崩溃。
66 |
67 | Fortunately, the techniques to avoid creating a big ball of mud aren't complex.
68 |
69 | 幸运的是,避免形成大泥球的技术并不复杂。
70 |
71 | // IDEA: talk about how architecture enables TDD and DDD (ie callback to book
72 | // subtitle)
73 |
74 | === Encapsulation and Abstractions
75 | 封装与抽象
76 |
77 | Encapsulation and abstraction are tools that we all instinctively reach for
78 | as programmers, even if we don't all use these exact words. Allow us to dwell
79 | on them for a moment, since they are a recurring background theme of the book.
80 |
81 | 封装和抽象是我们作为程序员本能地会使用的工具,即使我们并不总是使用这些确切的术语。
82 | 请允许我们稍作停留来讨论它们,因为它们是本书反复出现的背景主题。
83 |
84 | The term _encapsulation_ covers two closely related ideas: simplifying
85 | behavior and hiding data. In this discussion, we're using the first sense. We
86 | encapsulate behavior by identifying a task that needs to be done in our code
87 | and giving that task to a well-defined object or function. We call that object or function an
88 | _abstraction_.
89 |
90 | 术语 _封装_ 涵盖了两个密切相关的概念:简化行为和隐藏数据。在此讨论中,我们采用第一种含义。
91 | 通过识别代码中需要完成的任务并将其交给一个定义良好的对象或函数,我们实现了对行为的封装。
92 | 我们将这个对象或函数称为一个 _抽象_。
93 |
94 | //DS: not sure I agree with this definition. more about establishing boundaries?
95 |
96 | Take a look at the following two snippets of Python code:
97 | 来看以下两个 _Python_ 代码片段:
98 |
99 |
100 | [[urllib_example]]
101 | .Do a search with urllib(使用 urllib 进行搜索)
102 | ====
103 | [source,python]
104 | ----
105 | import json
106 | from urllib.request import urlopen
107 | from urllib.parse import urlencode
108 |
109 | params = dict(q='Sausages', format='json')
110 | handle = urlopen('http://api.duckduckgo.com' + '?' + urlencode(params))
111 | raw_text = handle.read().decode('utf8')
112 | parsed = json.loads(raw_text)
113 |
114 | results = parsed['RelatedTopics']
115 | for r in results:
116 | if 'Text' in r:
117 | print(r['FirstURL'] + ' - ' + r['Text'])
118 | ----
119 | ====
120 |
121 | [[requests_example]]
122 | .Do a search with requests(使用 requests 进行搜索)
123 | ====
124 | [source,python]
125 | ----
126 | import requests
127 |
128 | params = dict(q='Sausages', format='json')
129 | parsed = requests.get('http://api.duckduckgo.com/', params=params).json()
130 |
131 | results = parsed['RelatedTopics']
132 | for r in results:
133 | if 'Text' in r:
134 | print(r['FirstURL'] + ' - ' + r['Text'])
135 | ----
136 | ====
137 |
138 | Both code listings do the same thing: they submit form-encoded values
139 | to a URL in order to use a search engine API. But the second is simpler to read
140 | and understand because it operates at a higher level of abstraction.
141 |
142 | 两个代码示例实现的功能相同:它们将表单编码的值提交到一个 URL 以使用搜索引擎 API。
143 | 但第二个示例更易于阅读和理解,因为它是在更高层次的抽象上操作的。
144 |
145 | We can take this one step further still by identifying and naming the task we
146 | want the code to perform for us and using an even higher-level abstraction to make
147 | it explicit:
148 |
149 | 我们还可以更进一步,通过明确识别并命名我们希望代码为我们执行的任务,并使用一个更高层次的抽象来使其更清晰:
150 |
151 | [[ddg_example]]
152 | .Do a search with the duckduckgo client library(使用 DuckDuckGo 客户端库进行搜索)
153 | ====
154 | [source,python]
155 | ----
156 | import duckduckpy
157 | for r in duckduckpy.query('Sausages').related_topics:
158 | print(r.first_url, ' - ', r.text)
159 | ----
160 | ====
161 |
162 | Encapsulating behavior by using abstractions is a powerful tool for making
163 | code more expressive, more testable, and easier to maintain.
164 |
165 | 通过使用抽象来封装行为是一种强大的工具,可以使代码更具表达力、更易于测试并更易于维护。
166 |
167 | NOTE: In the literature of the object-oriented (OO) world, one of the classic
168 | characterizations of this approach is called
169 | http://www.wirfs-brock.com/Design.html[_responsibility-driven design_];
170 | it uses the words _roles_ and _responsibilities_ rather than _tasks_.
171 | The main point is to think about code in terms of behavior, rather than
172 | in terms of data or algorithms.footnote:[If you've come across
173 | class-responsibility-collaborator (CRC) cards, they're
174 | driving at the same thing: thinking about _responsibilities_ helps you decide how to split things up.]
175 | 在面向对象(OO)领域的相关文献中,这种方法的一个经典定义被称为 [责任驱动设计](http://www.wirfs-brock.com/Design.html)(_responsibility-driven design_);
176 | 它使用 _角色_ 和 _责任_ 这些术语,而不是 _任务_。核心思想是以行为的角度思考代码,而不是以数据或算法为中心。
177 | 脚注:[如果你接触过类-责任-协作(CRC)卡片,它们的目标是一样的:通过思考 _责任_,帮助你决定如何划分代码。]
178 |
179 | .Abstractions and ABCs
180 | 抽象与抽象基类(ABCs)
181 | *******************************************************************************
182 | In a traditional OO language like Java or C#, you might use an abstract base
183 | class (ABC) or an interface to define an abstraction. In Python you can (and we
184 | sometimes do) use ABCs, but you can also happily rely on duck typing.
185 |
186 | 在像 Java 或 C# 这样的传统面向对象语言中,你可能会使用抽象基类(ABC)或接口来定义一个抽象。
187 | 在 _Python_ 中,你可以(我们有时也确实会)使用抽象基类,但也完全可以愉快地依赖于鸭子类型。
188 |
189 | The abstraction can just mean "the public API of the thing you're using"—a
190 | function name plus some arguments, for example.
191 |
192 | 抽象可以仅仅表示“你正在使用的事物的公共 API”——例如,一个函数名加上一些参数。
193 | *******************************************************************************
194 |
195 | Most of the patterns in this book involve choosing an abstraction, so you'll
196 | see plenty of examples in each chapter. In addition,
197 | <> specifically discusses some general heuristics
198 | for choosing abstractions.
199 |
200 | 本书中的大多数模式都涉及选择抽象,因此你将在每一章中看到大量的示例。
201 | 此外,<> 专门讨论了一些关于选择抽象的一般性启发法。
202 |
203 | === Layering
204 | 分层
205 |
206 | Encapsulation and abstraction help us by hiding details and protecting the
207 | consistency of our data, but we also need to pay attention to the interactions
208 | between our objects and functions. When one function, module, or object uses
209 | another, we say that the one _depends on_ the other. These dependencies form a
210 | kind of network or graph.
211 |
212 | 封装和抽象通过隐藏细节和保护数据的一致性来帮助我们,但我们还需要关注对象和函数之间的交互。
213 | 当一个函数、模块或对象使用另一个时,我们称前者 _依赖于_ 后者。这些依赖关系构成了一种网络或图。
214 |
215 | In a big ball of mud, the dependencies are out of control (as you saw in
216 | <>). Changing one node of the graph becomes difficult because it
217 | has the potential to affect many other parts of the system. Layered
218 | architectures are one way of tackling this problem. In a layered architecture,
219 | we divide our code into discrete categories or roles, and we introduce rules
220 | about which categories of code can call each other.
221 |
222 | 在一个大泥球系统中,依赖关系是失控的(如你在 <> 中所见)。修改图中的一个节点变得困难,
223 | 因为它可能会影响系统的许多其他部分。分层架构是应对这一问题的一种方法。在分层架构中,
224 | 我们将代码划分为不同的类别或角色,并引入关于哪些类别的代码可以相互调用的规则。
225 |
226 | One of the most common examples is the _three-layered architecture_ shown in
227 | <>.
228 |
229 | 最常见的例子之一是 <> 中展示的 _三层架构_。
230 |
231 | [role="width-75"]
232 | [[layered_architecture1]]
233 | .Layered architecture(分层架构)
234 | image::images/apwp_0002.png[]
235 | [role="image-source"]
236 | ----
237 | [ditaa, apwp_0002]
238 | +----------------------------------------------------+
239 | | Presentation Layer |
240 | +----------------------------------------------------+
241 | |
242 | V
243 | +----------------------------------------------------+
244 | | Business Logic |
245 | +----------------------------------------------------+
246 | |
247 | V
248 | +----------------------------------------------------+
249 | | Database Layer |
250 | +----------------------------------------------------+
251 | ----
252 |
253 |
254 | Layered architecture is perhaps the most common pattern for building business
255 | software. In this model we have user-interface components, which could be a web
256 | page, an API, or a command line; these user-interface components communicate
257 | with a business logic layer that contains our business rules and our workflows;
258 | and finally, we have a database layer that's responsible for storing and retrieving
259 | data.
260 |
261 | 分层架构可能是构建业务软件中最常见的模式。在这种模型中,我们有用户界面组件,可以是网页、API 或命令行;
262 | 这些用户界面组件与包含业务规则和工作流程的业务逻辑层通信;最后,我们有一个数据库层,负责数据的存储和检索。
263 |
264 | For the rest of this book, we're going to be systematically turning this
265 | model inside out by obeying one simple principle.
266 |
267 | 在本书的其余部分,我们将通过遵守一个简单的原则,系统性地将这种模型翻转过来。
268 |
269 | [[dip]]
270 | === The Dependency Inversion Principle
271 | 依赖倒置原则
272 |
273 | You might be familiar with the _dependency inversion principle_ (DIP) already, because
274 | it's the _D_ in SOLID.footnote:[SOLID is an acronym for Robert C. Martin's five principles of object-oriented
275 | design: single responsibility, open for extension but
276 | closed for modification, Liskov substitution, interface segregation, and
277 | dependency inversion. See https://oreil.ly/UFM7U["S.O.L.I.D: The First 5 Principles of Object-Oriented Design"] by Samuel Oloruntoba.]
278 |
279 | 你可能已经熟悉 _依赖倒置原则_(DIP),因为它是 SOLID 原则中的 _D_。脚注:[SOLID 是 Robert C. Martin 提出的五大面向对象设计原则的首字母缩写:
280 | 单一责任原则(Single responsibility)、开放封闭原则(Open for extension but closed for modification)、
281 | 里氏替换原则(Liskov substitution)、接口隔离原则(Interface segregation)
282 | 以及依赖倒置原则(Dependency inversion)。
283 | 参见 Samuel Oloruntoba 的文章 [“S.O.L.I.D: The First 5 Principles of Object-Oriented Design”](https://oreil.ly/UFM7U)。]
284 |
285 | Unfortunately, we can't illustrate the DIP by using three tiny code listings as
286 | we did for encapsulation. However, the whole of <> is essentially a worked
287 | example of implementing the DIP throughout an application, so you'll get
288 | your fill of concrete examples.
289 |
290 | 遗憾的是,我们无法像讲解封装那样通过三个小代码示例来说明依赖倒置原则(DIP)。然而,
291 | <> 的全部内容本质上就是一个在整个应用程序中实现 DIP 的完整示例,因此你会看到大量具体的示例。
292 |
293 | In the meantime, we can talk about DIP's formal definition:
294 | 与此同时,我们可以讨论一下依赖倒置原则(DIP)的正式定义:
295 |
296 | // [SG] reference?
297 |
298 | 1. High-level modules should not depend on low-level modules. Both should
299 | depend on abstractions.
300 | 高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
301 |
302 | 2. Abstractions should not depend on details. Instead, details should depend on
303 | abstractions.
304 | 抽象不应该依赖于细节。相反,细节应该依赖于抽象。
305 |
306 | But what does this mean? Let's take it bit by bit.
307 |
308 | 但这是什么意思呢?让我们一点一点地解析。
309 |
310 | _High-level modules_ are the code that your organization really cares about.
311 | Perhaps you work for a pharmaceutical company, and your high-level modules deal
312 | with patients and trials. Perhaps you work for a bank, and your high-level
313 | modules manage trades and exchanges. The high-level modules of a software
314 | system are the functions, classes, and packages that deal with our real-world
315 | concepts.
316 |
317 | _高层模块_ 是你的组织真正关心的代码。也许你为一家制药公司工作,高层模块处理患者和试验。
318 | 也许你为一家银行工作,高层模块负责管理交易和兑换。软件系统的高层模块是那些处理现实世界概念的函数、类和包。
319 |
320 | By contrast, _low-level modules_ are the code that your organization doesn't
321 | care about. It's unlikely that your HR department gets excited about filesystems or network sockets. It's not often that you discuss SMTP, HTTP,
322 | or AMQP with your finance team. For our nontechnical stakeholders, these
323 | low-level concepts aren't interesting or relevant. All they care about is
324 | whether the high-level concepts work correctly. If payroll runs on time, your
325 | business is unlikely to care whether that's a cron job or a transient function
326 | running on Kubernetes.
327 |
328 | 相比之下,_低层模块_ 是你的组织并不关心的代码。你的 HR 部门不太可能对文件系统或网络套接字感到兴奋。
329 | 你也不太会与财务团队讨论 SMTP、HTTP 或 AMQP 等技术细节。对于非技术型利益相关者来说,
330 | 这些低层次的概念既不有趣也无关紧要。他们关心的只是高层次的概念是否能够正常运行。
331 | 如果工资按时发放,你的企业大概率不会在意这背后是使用 cron 任务还是运行在 Kubernetes 上的某个临时函数。
332 |
333 | _Depends on_ doesn't mean _imports_ or _calls_, necessarily, but rather a more
334 | general idea that one module _knows about_ or _needs_ another module.
335 |
336 | _依赖于_ 并不一定意味着 _导入_ 或 _调用_,而是更为广泛的概念,指一个模块 _了解_ 或 _需要_ 另一个模块。
337 |
338 | And we've mentioned _abstractions_ already: they're simplified interfaces that
339 | encapsulate behavior, in the way that our duckduckgo module encapsulated a
340 | search engine's API.
341 |
342 | 我们已经提到过 _抽象_:它们是封装行为的简化接口,就像我们的 duckduckgo 模块封装了一个搜索引擎的 API 一样。
343 |
344 | [quote,David Wheeler]
345 | ____
346 | All problems in computer science can be solved by adding another level of
347 | indirection.
348 |
349 | 计算机科学中的所有问题都可以通过增加一个间接层来解决。
350 | ____
351 |
352 | So the first part of the DIP says that our business code shouldn't depend on
353 | technical details; instead, both should use abstractions.
354 |
355 | 因此,依赖倒置原则(DIP)的第一部分表明,我们的业务代码不应该依赖于技术细节;相反,两者都应该使用抽象。
356 |
357 | Why? Broadly, because we want to be able to change them independently of each
358 | other. High-level modules should be easy to change in response to business
359 | needs. Low-level modules (details) are often, in practice, harder to
360 | change: think about refactoring to change a function name versus defining, testing,
361 | and deploying a database migration to change a column name. We don't
362 | want business logic changes to slow down because they are closely coupled
363 | to low-level infrastructure details. But, similarly, it is important to _be
364 | able_ to change your infrastructure details when you need to (think about
365 | sharding a database, for example), without needing to make changes to your
366 | business layer. Adding an abstraction between them (the famous extra
367 | layer of indirection) allows the two to change (more) independently of each
368 | other.
369 |
370 | 为什么呢?总的来说,是因为我们希望能够让它们彼此独立地进行更改。高层模块应该能够轻松地根据业务需求进行修改。
371 | 而低层模块(细节)在实践中通常更难更改:例如,重构一个函数名相对简单,而定义、测试并部署一个用于修改数据库列名的迁移却要复杂得多。
372 | 我们不希望因为业务逻辑与底层基础设施的细节紧密耦合而导致业务逻辑的变更变得缓慢。
373 | 但同样重要的是,当需要时,我们必须能够更改你的基础设施细节(例如,分片数据库),而无需对业务层进行修改。
374 | 在它们之间添加一个抽象层(著名的额外间接层)可以让两者(更)独立地进行变更。
375 |
376 | The second part is even more mysterious. "Abstractions should not depend on
377 | details" seems clear enough, but "Details should depend on abstractions" is
378 | hard to imagine. How can we have an abstraction that doesn't depend on the
379 | details it's abstracting? By the time we get to <>,
380 | we'll have a concrete example that should make this all a bit clearer.
381 |
382 |
383 | 第二部分就更加玄妙了。“抽象不应该依赖于细节”似乎很容易理解,但“细节应该依赖于抽象”却难以想象。
384 | 我们如何能有一个抽象而不依赖于它所抽象的那些细节呢?等我们到了 <> 时,
385 | 将会有一个具体的例子,可以让这一切变得更清晰一些。
386 |
387 | === A Place for All Our Business Logic: The Domain Model
388 | 为我们的业务逻辑提供一个归宿:领域模型
389 |
390 | But before we can turn our three-layered architecture inside out, we need to
391 | talk more about that middle layer: the high-level modules or business
392 | logic. One of the most common reasons that our designs go wrong is that
393 | business logic becomes spread throughout the layers of our application,
394 | making it hard to identify, understand, and change.
395 |
396 | 但是,在我们将三层架构翻转之前,我们需要深入讨论中间层:高级模块或业务逻辑。我们的设计出错的一个最常见原因是,
397 | 业务逻辑分散在应用程序的各个层中,这使得辨识、理解和更改变得困难。
398 |
399 | <> shows how to build a business
400 | layer with a _Domain Model_ pattern. The rest of the patterns in <> show
401 | how we can keep the domain model easy to change and free of low-level concerns
402 | by choosing the right abstractions and continuously applying the DIP.
403 |
404 | <> 展示了如何使用 _Domain Model_ 模式构建业务层。
405 | <> 中的其余模式则展示了如何通过选择合适的抽象并持续应用DIP(依赖倒置原则),使领域模型易于更改并避免低层次的关注点。
406 |
--------------------------------------------------------------------------------
/ix.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | Creative Commons CC-By-ND
2 |
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | ignore_missing_imports = False
3 | namespace_packages = True
4 | mypy_path = ./code/src
5 | check_untyped_defs = True
6 |
7 | [mypy-pytest.*,lxml.*,sqlalchemy.*,redis.*,django.*]
8 | ignore_missing_imports = True
9 |
--------------------------------------------------------------------------------
/outline.md:
--------------------------------------------------------------------------------
1 | # Outline
2 |
3 | ## Preface/Intro
4 |
5 | Who are we, why we are writing this book
6 | (all the args from the proposal, python's popularity, communicating well-understood patterns from the Java/C#/Enterprise world to a wider audience with nice, readable, pythonic code examples)
7 |
8 | > As Python grows in popularity as a language, typical projects are getting larger and more complex, and issues of software design and architecture become more salient. Patterns like "Ports and Adapters", as well Domain-Driven Design, Event-Driven programming, Command-Query Responsibility Segregation, which are well known in more "enterprisey" communities like Java and C#, are generating more interest in the Python community, but it's not always easy to see how these patterns translate into Python. (reviewing the classic "Gang of Four" design patterns book for example leads to the conclusion that half the patterns are artifacts of the Java/C++ syntax, and are simply not necessary in the more flexible and dynamic world of Python).
9 |
10 | > In the Python world, we often quote the Zen of Python: "there should be one--preferably on only one--obvious way to do it". Unfortunately, as project complexity grows, the most obvious way of doing things isn't always the way that helps you manage complexity and evolving requirements.
11 |
12 | > This book will provide an introduction to proven architectural design patterns that help you manage complexity, using concrete examples written in Python. It will explain how to avoid some of the unnecessary particularities of Java and C# syntax and implement these patterns in a "Pythonic" way.
13 |
14 | An overview of the central argument:
15 |
16 | > Layer your system so that the low-level details depend on the high-level abstractions, not the other way around.
17 |
18 |
19 | ## Chapter 1: Domain modelling, and why do we always make it so hard for ourselves?
20 |
21 | Every so often someone says "where shall I put this new logic", and we all know the right answer: it should live in the domain model. So why does it always somehow end up in some gigantic controller function that's closely coupled with your web framework and database and third party apis and god knows what else?
22 |
23 | Let's see what happens if we build everything around making sure our domain model is easy to work with and change.
24 |
25 | * Talk about domain modelling and DDD, work through an example (allocate-order)
26 | * Example code
27 | * crazy idea: Bob and I will record a video of (part of?) the allocate-order code writing as a TDD "kata", to illustrate how easy it is to do this stuff when you have no dependencies on databases, frameworks, etc (this will also save us from having to go slowly thru what tdd is in the book)
28 |
29 | code examples / patterns: some domain model objects (python3.7 dataclasses maybe?), a domain service / use case function, some "proper" unit tests.
30 |
31 | related post from existing blog: https://io.made.com/introducing-command-handler/
32 |
33 |
34 |
35 | ## Chapter 2: persistence and the repository pattern
36 |
37 | The main ORM tool in the Python world is SQLAlchemy, and if you follow the default tutorial, you end up writing your model objects inheriting from `sqlalchemy.Table`, and soon your domain model is tightly coupled to your DB.
38 |
39 | But you don't have to! Demonstrate the alternative way to do metadata/mapping.
40 |
41 | ==> our ORM depends on the domain model, and not the other way around. an illustration of one of our key patterns/principles, the _Dependency Inversion Principle_ (the D in SOLID)
42 |
43 | Also: repository pattern. choosing a simple abstraction (it's a dictionary)
44 | Also: first integration test
45 |
46 | code examples / patterns: sqlalchemy metadata/mapping, repository pattern
47 |
48 | related post from existing blog: https://io.made.com/repository-and-unit-of-work-pattern-in-python/
49 |
50 |
51 |
52 | ## Chapter 3: making ourselves available via a web API. Flask as a port (as in ports-and-adapters). Our first use case. Orchestration. Service layer
53 |
54 | We have a use case in mind, and a domain model that can do it, but how do we make it available to the outside world?
55 |
56 | start with a naive flask controller. evolve it to flask being an adapter to a use case function in our service/orchestration layer.
57 |
58 | * happy path, show the basic use case moving parts: create a database session, initialise our repository, load some objects, invoke our domain function, commit.
59 | * first acceptance test
60 | * handle error case, eg product does not exist. handle `KeyError` from repository, flask returns a 400 with nice erro json
61 | * but what if we have more error cases at this orchestration level? it'll be annoying to test everything with acceptance tests, and hard to unit test.
62 |
63 | ==> introduce service layer. flask becomes an adapter. flask depends on the service layer, rather than the other way around (DIP once again)
64 |
65 | (later in the book we'll swap flask for asyncio, and show how easy it is)
66 |
67 | patterns: use case, service layer, port/adapter pattern for web,
68 |
69 | related post from existing blog: https://io.made.com/repository-and-unit-of-work-pattern-in-python/
70 |
71 |
72 |
73 | ## Chapter 4: data integrity concerns 1: unit of work pattern
74 |
75 | What happens if we encounter an error during our allocation? eg out of stock, a domain error? We'd like to wrap our work up so that, either the entire order is allocated, or we abort and leave things in a "clean" state if anything goes wrong -- a classic case for a transaction/rollback.
76 |
77 | What's a Pythonic way of "doing transactions"? A context manager. demonstrate the _Unit of Work Pattern_ and show how it fits with _Repository_
78 | But we also want a nice, Pythonic way of "doing transactions", of wrapping a block of code up into an atomic whole.
79 |
80 | discuss different options of unit of work, explicit/implicit rollbacks, dependency-inject uow.
81 |
82 | code examples / patterns: Unit Of Work (using a context manager)
83 |
84 | related post from existing blog: https://io.made.com/repository-and-unit-of-work-pattern-in-python/
85 |
86 |
87 | ## Chapter 5: data integrity concerns 2: choosing the right consistency boundary (Aggregate pattern)
88 |
89 | While we're thinking about data integrity, we now realise that our `allocate(order, shipments)` implementation which depends on all the shipments in the world won't scale if every order needs to lock the whole shipments table. We should only look at shipments that matter to that order.
90 |
91 | Moreover, we only need to allocate the order one line at a time (although maybe we want to roll back all the lines if we fail any one of them).
92 |
93 | This leads us on to discussing the _Aggregate_ pattern - by choosing `Product` as our aggregate, we choose a consistency boundary that allows us to be more clever about transactions.
94 |
95 | Also demonstrate how easy it is to refactor a domain model if it's not intermixed with infrastructure concerns.
96 |
97 | code examples / patterns: Aggregate
98 |
99 |
100 |
101 | ## Chapter 6: event-driven architecture part 1: doamin events and the message bus
102 |
103 | Another new requirement: when allocation fails because we are out of stock, someone should be emailed. But we don't want to have email-sending code be a potential cause of bugs/failures in our core model/state changes. introduce domain events and a message bus as a pattern for kicking off related work after a use case is complete.
104 |
105 | * discuss SRP, use case shouldn't have an _and_. leads naturally to events.
106 |
107 | code examples / patterns: events, handlers, message bus
108 |
109 | related post from existing blog: https://io.made.com/why-use-domain-events/
110 |
111 |
112 |
113 | ## Chapter 7: event-driven architecture part 2: reactive microservices
114 |
115 | We've got a microservice with an web api, but what about other ways of talking to other systems? how does it know if, say, a
116 | shipment is delayed or the quantity is amended? how does it communicate to our warehouse system to say that an order has
117 | been allocated and needs to be sent to a customer?
118 |
119 | * redis pubsub in: batch_quantity_changed. leads to deallocate / reallocate
120 | * redis pubsub out: order_allocated event, for communicating with warehouse
121 |
122 | code examples / patterns: events as a microservices integration platform
123 |
124 |
125 | ## Chapter 8: Commands vs Events
126 |
127 | distinguish commands from events. different semantics, can say no to a command.
128 |
129 |
130 |
131 | ## Chapter 9: CQRS
132 |
133 | The business comes along and supplies a new requirement: a dashboard showing the current allocation state of all shipments. Discuss how using the ORM naively leads to the _SELECT N+1_ antipattern, and use it as an opportunity to demonstrate _Command-Query-Responsiblity-Segregation (CQRS)_ -- read-only parts of our architecture can be implemented quite differently from the write side
134 |
135 | start with raw sql
136 | discuss option of using events to update a view model
137 |
138 | code examples / patterns: CQRS / event-driven view model.
139 |
140 | related post from existing blog: https://io.made.com/commands-and-queries-handlers-and-views/
141 |
142 |
143 | ## Chapter 10: Bootstrap.py and dependency injection
144 |
145 | the database / sqlalchemy are under control, but email and redis are a bit haphazard. also orm initialisation. show how a bootsrap script
146 | and DI can manage all this
147 |
148 | diagrams.
149 |
150 | with + without framework, `@inject`, and bob's crazy, heretical, unclean type-hints based one.
151 |
152 | maybe point out that command-handler pattern would make this all easier?
153 |
154 | related post from existing blog: https://io.made.com/dependency-injection-with-type-signatures-in-python/
155 |
156 |
157 | ## Appendix 1: project structure
158 |
159 | show folder structure, dockerfiles, setup.py, where tests live, etc.
160 |
161 |
162 |
163 | ## Appendix 2: swapping out flask and our database for a CLI with CSVs
164 |
165 | (follows on from unit of work chapter).
166 | the business come to us apologetically saying they're not ready to use our API and could we build a thing that reads batches and orders from 2 csvs and outputs a third with allocations".
167 |
168 | show how by just changing our repository and unitofwork, we can use the exact same service layer and domain layer to build a CLI app.
169 |
170 | this could be an exercise for the reader tbh. or a video
171 |
172 |
173 | ## appendix 3: command handler pattern?
174 |
175 | ==> show how commands can be put on the message bus just like events.
176 |
177 | code examples / patterns: reuse message bus for commands
178 |
179 | related post from existing blog: https://io.made.com/introducing-command-handler/
180 |
181 |
182 |
183 |
--------------------------------------------------------------------------------
/part1.asciidoc:
--------------------------------------------------------------------------------
1 | [role="pagenumrestart"]
2 | [[part1]]
3 | [part]
4 | == Building an Architecture to Support Domain Modeling
5 | 构建支持领域建模的架构
6 |
7 |
8 | [quote, Cyrille Martraire, DDD EU 2017]
9 | ____
10 | Most developers have never seen a domain model, only a data model.
11 | 大多数开发者只见过数据模型,而从未见过领域模型。
12 | ____
13 |
14 | Most developers we talk to about architecture have a nagging sense that
15 | things could be better. They are often trying to rescue a system that has gone
16 | wrong somehow, and are trying to put some structure back into a ball of mud.
17 | They know that their business logic shouldn't be spread all over the place,
18 | but they have no idea how to fix it.
19 |
20 | 我们和大多数开发者谈论架构时,他们通常都有一种挥之不去的感觉:现状本可以更好。很多时候,他们在试图拯救一个以某种方式陷入混乱的系统,
21 | 并努力在一团乱麻中重建一些结构。他们知道业务逻辑不应该到处散落,但却不知道该如何解决这个问题。
22 |
23 | We've found that many developers, when asked to design a new system, will
24 | immediately start to build a database schema, with the object model treated
25 | as an afterthought. This is where it all starts to go wrong. Instead, _behavior
26 | should come first and drive our storage requirements._ After all, our customers don't care about the data model. They care about what
27 | the system _does_; otherwise they'd just use a spreadsheet.
28 |
29 | 我们发现,许多开发者在被要求设计一个新系统时,会直接从构建数据库模式入手,而将对象模型当作事后补充。这正是问题开始出错的地方。
30 | 实际上,_行为应该是首要的,并驱动我们的存储需求。_ 毕竟,客户并不关心数据模型,他们关心的是系统 _做了什么_;否则,他们就直接使用电子表格了。
31 |
32 | The first part of the book looks at how to build a rich object model
33 | through TDD (in <>), and then we'll show how
34 | to keep that model decoupled from technical concerns. We show how to build
35 | persistence-ignorant code and how to create stable APIs around our domain so
36 | that we can refactor aggressively.
37 |
38 | 本书的第一部分将探讨如何通过TDD构建一个丰富的对象模型(在<>中),随后我们将展示如何让该模型与技术问题解耦。
39 | 我们会讲解如何构建与持久化无关的代码,以及如何围绕我们的领域创建稳定的API,从而使我们能够进行积极的重构。
40 |
41 | To do that, we present four key design patterns:
42 |
43 | 为此,我们将介绍四个关键的设计模式:
44 |
45 | * The <>, an abstraction over the
46 | idea of persistent storage
47 | (<>,一种对持久化存储概念的抽象。)
48 |
49 | * The <> to clearly define where our
50 | use cases begin and end
51 | (<>,用于清晰地定义我们的用例从哪里开始以及在哪里结束。)
52 |
53 | [role="pagebreak-before"]
54 | * The <> to provide atomic operations
55 | (<>,用于提供原子操作。)
56 |
57 | * The <> to enforce the integrity
58 | of our data
59 | (<>,用于确保数据的完整性。)
60 |
61 | If you'd like a picture of where we're going, take a look at
62 | <>, but don't worry if none of it makes sense
63 | yet! We introduce each box in the figure, one by one, throughout this part of the book.
64 |
65 | 如果你想了解我们接下来的内容,可以看看<>,不过如果现在还不明白也别担心!
66 | 我们会在本书的这一部分中逐一介绍图中的每个模块。
67 |
68 | [role="width-90"]
69 | [[part1_components_diagram]]
70 | .A component diagram for our app at the end of <>(在 <> 末尾,我们应用的组件图)
71 | image::images/apwp_p101.png[]
72 |
73 | We also take a little time out to talk about
74 | <>, illustrating it with a simple example that shows how and why we choose our
75 | abstractions.
76 |
77 | 我们还会花一些时间讨论<>,并通过一个简单的示例来说明我们是如何以及为什么选择抽象的。
78 |
79 | Three appendices are further explorations of the content from Part I:
80 |
81 | 有三个附录进一步探讨了第一部分的内容:
82 |
83 | * <> is a write-up of the infrastructure for our example
84 | code: how we build and run the Docker images, where we manage configuration
85 | info, and how we run different types of tests.
86 | <> 详细介绍了我们示例代码的基础设施:我们如何构建和运行Docker镜像、如何管理配置信息,
87 | 以及如何运行不同类型的测试。
88 |
89 | * <> is a "proof of the pudding" kind of content, showing
90 | how easy it is to swap out our entire infrastructure--the Flask API, the
91 | ORM, and Postgres—for a totally different I/O model involving a CLI and
92 | CSVs.
93 | <> 是一种“实践检验”的内容,展示了将整个基础设施(如Flask API、ORM和Postgres)替换为
94 | 完全不同的I/O模型(包括CLI和CSV文件)是多么简单。
95 |
96 | * Finally, <> may be of interest if you're wondering how these
97 | patterns might look if using Django instead of Flask and SQLAlchemy.
98 | 最后,如果你想了解在使用Django而不是Flask和SQLAlchemy时这些模式会是什么样子,可以参考<>。
99 |
--------------------------------------------------------------------------------
/part2.asciidoc:
--------------------------------------------------------------------------------
1 | [[part2]]
2 | [part]
3 | == Event-Driven Architecture
4 | 事件驱动架构
5 |
6 | [quote, Alan Kay]
7 | ____
8 |
9 | I'm sorry that I long ago coined the term "objects" for this topic because it
10 | gets many people to focus on the lesser idea.
11 |
12 | 我很抱歉自己很早就为这个主题创造了“对象”这个术语,因为它让许多人将注意力集中在了次要的概念上。
13 |
14 | The big idea is "messaging."...The key in making great and growable systems is
15 | much more to design how its modules communicate rather than what their internal
16 | properties and behaviors should be.
17 |
18 | 核心思想是“消息传递”……构建优秀且可扩展系统的关键更多在于设计模块之间如何通信,而不是它们的内部属性和行为应该是什么样的。
19 | ____
20 |
21 | It's all very well being able to write _one_ domain model to manage a single bit
22 | of business process, but what happens when we need to write _many_ models? In
23 | the real world, our applications sit within an organization and need to exchange
24 | information with other parts of the system. You may remember our context
25 | diagram shown in <>.
26 |
27 | 能够编写 _一个_ 领域模型来管理单一业务流程当然很好,但是当我们需要编写 _多个_ 模型时会发生什么呢?在现实世界中,我们的应用程序位于一个组织内,
28 | 并且需要与系统的其他部分交换信息。你或许还记得我们在<>中展示的上下文图。
29 |
30 | Faced with this requirement, many teams reach for microservices integrated
31 | via HTTP APIs. But if they're not careful, they'll end up producing the most
32 | chaotic mess of all: the distributed big ball of mud.
33 |
34 | 面对这一需求,许多团队会选择通过HTTP API集成的微服务架构。但如果不小心,他们最终可能会制造出最混乱的局面:分布式的“大泥球”。
35 |
36 | In Part II, we'll show how the techniques from <> can be extended to
37 | distributed systems. We'll zoom out to look at how we can compose a system from
38 | many small components that interact through asynchronous message passing.
39 |
40 | 在第二部分中,我们将展示如何将<>中的技术扩展到分布式系统。我们将放大视角,探讨如何通过异步消息传递将多个小组件组合成一个系统。
41 |
42 | We'll see how our Service Layer and Unit of Work patterns allow us to reconfigure our app
43 | to run as an asynchronous message processor, and how event-driven systems help
44 | us to decouple aggregates and applications from one another.
45 |
46 | 我们将看到如何利用服务层模式和工作单元模式,将我们的应用程序重新配置为一个异步消息处理器,以及事件驱动系统如何帮助我们实现聚合与应用程序之间的解耦。
47 |
48 | [[allocation_context_diagram_again]]
49 | .But exactly how will all these systems talk to each other?(但这些系统究竟如何相互通信呢?)
50 | image::images/apwp_0102.png[]
51 |
52 |
53 | // TODO: DS - this might give the impression that the whole of part 2
54 | // is irrelevant for readers in a monolith context
55 |
56 | //IDEA (DS): It seems to me the two key themes in this book are vertical and
57 | //horizontal decoupling. Did you consider choosing those for the two parts?
58 |
59 | We'll look at the following patterns and techniques:
60 |
61 | 我们将探讨以下模式和技术:
62 |
63 | Domain Events(领域事件)::
64 | Trigger workflows that cross consistency boundaries.
65 | 触发跨越一致性边界的工作流。
66 |
67 | Message Bus(消息总线)::
68 | Provide a unified way of invoking use cases from any endpoint.
69 | 提供一种从任何端点调用用例的统一方式。
70 |
71 | CQRS(命令查询责任分离)::
72 | Separating reads and writes avoids awkward compromises in an event-driven
73 | architecture and enables performance and scalability improvements.
74 | 将读取和写入分离可以避免在事件驱动架构中出现尴尬的折中,并提升性能和可扩展性。
75 |
76 | Plus, we'll add a dependency injection framework. This has nothing to do with
77 | event-driven architecture per se, but it tidies up an awful lot of loose
78 | ends.
79 |
80 | 另外,我们还会引入一个依赖注入框架。虽然这本身与事件驱动架构无关,但它能整理好许多松散的部分。
81 |
82 | // IDEA: a bit of blurb about making events more central to our design thinking?
83 |
--------------------------------------------------------------------------------
/plantuml.cfg:
--------------------------------------------------------------------------------
1 | skinparam default {
2 | FontName Guardian Sans Cond Regular
3 | FontSize 18
4 | FontColor Black
5 | }
6 |
7 | skinparam class {
8 | BackgroundColor #b5e2fa
9 | BorderColor #0fa3b1
10 | }
11 |
12 | skinparam CircledCharacter {
13 | FontColor Black
14 | }
15 |
16 | skinparam stereotypeC {
17 | BackgroundColor #eddea4
18 | }
19 |
20 | skinparam package {
21 | FontName Guardian Sans Cond Light
22 | }
23 |
24 | skinparam sequencelifeline {
25 | BorderColor #0fa3b1
26 | }
27 | skinparam arrow {
28 | Color #0fa3b1
29 | }
30 | skinparam participant {
31 | BackgroundColor #b5e2fa
32 | BorderColor #0fa3b1
33 | }
34 | skinparam entity {
35 | BackgroundColor #b5e2fa
36 | BorderColor #0fa3b1
37 | }
38 | skinparam collections {
39 | BackgroundColor #b5e2fa
40 | BorderColor #0fa3b1
41 | }
42 | skinparam database {
43 | BackgroundColor #b5e2fa
44 | BorderColor #0fa3b1
45 | }
46 | skinparam boundary {
47 | BackgroundColor #b5e2fa
48 | BorderColor #0fa3b1
49 | }
50 | skinparam actor {
51 | Color DeepSkyBlue
52 | BackgroundColor #b5e2fa
53 | BorderColor #0fa3b1
54 | }
55 | skinparam sequencegroupheader {
56 | FontName Guardian Sans Cond Light
57 | }
58 | skinparam sequencebox {
59 | BackgroundColor PowderBlue
60 | BorderColor #0fa3b1
61 | }
62 | skinparam padding 4
63 |
--------------------------------------------------------------------------------
/print_figure_numbers_xref_to_image_filenames.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from pathlib import Path
3 | import re
4 |
5 | for path in sorted(Path(__file__).absolute().parent.glob('*.asciidoc')):
6 | images = re.findall(r'::images/(\w+\.png)', path.read_text())
7 | if not images:
8 | continue
9 | chapter_no = re.search(r'chapter_(\d\d)', str(path))
10 | if chapter_no:
11 | chapter_no = str(int(chapter_no.group(1)))
12 | else:
13 | chapter_no = '??'
14 | print(path.name)
15 | for ix, image in enumerate(images):
16 | print(f' Figure {chapter_no}.{ix+1}: {image}')
17 |
--------------------------------------------------------------------------------
/proposal.md:
--------------------------------------------------------------------------------
1 | # Proposed Book Title:
2 |
3 | * The Clean Architecture in Python?
4 | * Ports and Adapters with Python?
5 | * Enterprise Design Patterns in Python?
6 | * Enterprise Software Architecture in Python?
7 | * Software Architecture in Python?
8 |
9 | ## Proposed Book Subtitle:
10 |
11 | *How to apply DDD, Ports and Adapters and more enterprise architecture design patterns in a Pythonic way.*
12 |
13 | # Author(s):
14 |
15 | Bob Gregory, Harry Percival
16 |
17 | > Author title(s) and affiliation(s):
18 |
19 | Lead Architect and Software Developer (respectively) at MADE.com
20 |
21 | > Preferred mailing address(es):
22 | > Preferred phone number:
23 | > Preferred Email address(es):
24 | harry.percival@gmail.com, bob.gregory@made.com
25 | > Author Platform details:
26 | > Author biography and LinkedIn profile:
27 |
28 | https://uk.linkedin.com/in/harry-percival-588a35
29 | https://uk.linkedin.com/in/bobthemighty
30 |
31 |
32 | > Author public speaking samples (YouTube, etc.):
33 |
34 | https://www.youtube.com/watch?v=tFalO9KdCDM
35 | https://skillsmatter.com/skillscasts/12182-event-sourcing-101
36 |
37 | > Author Web site/blog/Twitter:
38 |
39 | https://io.made.com/
40 | http://www.obeythetestinggoat.com/
41 |
42 |
43 | # Why are you the best person to write this book?
44 |
45 | Harry has already written one excellent Python book for O'Reilly, this will make a nice sequel (in fact it covers some of the further reading subjects suggested in the final chapter of said book).
46 | Bob knows more than Harry about the subject matter though.
47 |
48 |
49 | # Book Summary:
50 |
51 | > In one sentence, tell us why the audience will want to buy your book.
52 |
53 | The Python world is increasingly interested in software architecture and design, and there are no good Python-specific books on the topic yet.
54 |
55 |
56 | > Summarize what the book is about, like you would pitch it to a potential reader on the back cover. What makes your book unique in the marketplace?
57 |
58 | As Python grows in popularity as a language, typical projects are getting larger and more complex, and issues of software design and architecture become more salient. Patterns like "Ports and Adapters", as well Domain-Driven Design, Event-Driven programming, Command-Query Responsibility Segregation, which are well known in more "enterprisey" communities like Java and C#, are generating more interest in the Python community, but it's not always easy to see how these patterns translate into Python. (reviewing the classic "Gang of Four" design patterns book for example leads to the conclusion that half the patterns are artifacts of the Java/C++ syntax, and are simply not necessary in the more flexible and dynamic world of Python).
59 |
60 | In the Python world, we often quote the Zen of Python: "there should be one--preferably on only one--obvious way to do it". Unfortunately, as project complexity grows, the most obvious way of doing things isn't always the way that helps you manage complexity and evolving requirements.
61 |
62 | This book will provide an introduction to proven architectural design patterns that help you manage complexity, using concrete examples written in Python. It will explain how to avoid some of the unnecessary particularities of Java and C# syntax and implement these patterns in a "Pythonic" way.
63 |
64 |
65 | # Technology summary.
66 |
67 | > How would you characterize the technology’s stage of development? (Put an X in the column next to the stage that best applies.)
68 |
69 | In the enterprise world: mature. In the Python world: developing.
70 |
71 |
72 | > Briefly explain the technology and why it is important.
73 |
74 | Design patterns and software architecture are well established topics in the enterprise software development world, but much less so in the Python world. As Python matures, translating these topics across is becoming more and more important.
75 |
76 |
77 |
78 | # Audience:
79 |
80 | > Explain who the primary audience is for your book. What professional positions does this audience hold? What knowledge do you assume of this audience? What books can you assume they have read? What skills can you assume they have mastered?
81 |
82 | This is an intermediate-level book. It will be of interest to anyone working on codebases of more than moderate complexity, and anyone with an interest in applying architectural patterns common in the C#/Java world to Python. It will also be of interest to software architects and developers coming from those communities and looking to adapt to the Python world. Finally the aim will be to make this the most accessible, engaging, and concrete introduction to the architectural concepts involved, such that programmers from any background will turn to it as a first resource.
83 |
84 | These people might have read:
85 |
86 | - my first book, and be wondering where to go next
87 | - classics like the Evans or Vernon DDD books, or Martin Fowler's "Patterns of Enterprise Application Architecture", and be wondering how to translate those concepts to the Python world
88 |
89 | > Please estimate as best you can how many people will use this technology? Please state any applicable statistics (e.g., web searches, web site traffic, blogs) indicating market use or market growth.
90 |
91 | Hard to say. salesrank data from amazon suggest books like "Buiding evolutionary architectures" (O'Reilly, https://www.novelrank.com/asin/1491986360) and "Patterns of Enterprise Application Architecture" (a classic, https://www.novelrank.com/asin/0321127420) are selling well (compared to my existing book lol)
92 |
93 | Other than that, informally, several people have reached out to us in response to our existing 5-part blog post series on the made.com blog, saying how much they'd like to see that content extended to book length, and complaining about the lack of similar resources in the Python world
94 |
95 |
96 |
97 | > Please provide some scenarios that indicate how the audience will use your book. For example, will readers refer to it daily as a reference? Will they read it once to learn the concepts and then refer to it occasionally?
98 |
99 | I expect most readers will read it all the way through once. Some may decide to follow along with the code examples in some or all of the chapters. Then, they are likely to come back and look at the code examples in more detail as they come to try and implement the various patterns in their day-to-day jobs.
100 |
101 | > Use the following table to describe how the audience for your book typically gets information and where it looks for guidance and leadership (list top five choices).
102 |
103 | > What web sites or blogs do they read?
104 |
105 | my blog, hacker news, /r/python, the made blog, follow on twitter bernhardt, fowler, beck, uncle martin...
106 |
107 | > What publications do they read (e.g., magazines, journals, newspapers)?
108 |
109 | see above.
110 |
111 | > What conferences do they attend?
112 |
113 | pycon(s), oreilly software architecture conference, oscon, fosdem,
114 |
115 |
116 | > Who are the leaders and key influencers in the field who would review or endorse your book?
117 |
118 | I could reach out via some contacts at Thoughtworks to see if Martin Fowler might give it a read. That would be a great name to have on the cover.
119 |
120 | In the Python world, Harry's existing Python contacts should be prepared to take a look -- Kenneth Reitz, Gary Bernhanrdt, Michael Foord.
121 |
122 |
123 |
124 |
125 |
126 | # Key Topic Coverage:
127 |
128 | > What are the top five topics that will be covered in the book? Why are they the top five?
129 |
130 | - the dependency inversion principle
131 | - ports and adapters
132 | - domain-driven design (DDD)
133 | - CQRS (command/query responsibility segregation)
134 | - event-driven architectures (and link to microservices)
135 |
136 | These are the most popular and recognisable words that people will recognise or have heard of, and appreciate seeing a concrete illustration and discussion of, in the Python world.
137 |
138 | > What problems does this book solve for its users?
139 |
140 | - How do I deal with increasing complexity as my application grows?
141 | - How can I learn about enterprise software design principles without wading through overcomplicated Java/C++ syntax?
142 | - Where can I find concrete examples rather than abstract discussion?
143 |
144 | > List the four or five topics covered or features included that will provide the greatest benefit to readers or will be the most likely to excite them?
145 |
146 | see above, plus:
147 |
148 | - How do I separate my domain model from infrastructure and integration layers?
149 | - Concretely, what is a Port and what is an Adapter in Python? Does the distinction matter?
150 | - What are the expected benefits of this sort of architecture? When is it worth implementing?
151 | - How do these patterns complement a microservices approach?
152 |
153 | > SEO terms for topics covered:
154 |
155 | see bullets above, plus "clean architecture", "hexagonal architecture", "functional core imperative shell",...
156 |
157 |
158 | # Other Book Features and Video Offerings:
159 |
160 | > Is there a companion web site? If so, what do you plan to include on the site? Would you be willing to participate in video offerings as well as workshops and training seminars?
161 |
162 | - a website with access to source code examples, and follow-on blog posts and materials is likely.
163 | - it could also include "alternative implementations" showing some other ways of achieving the same goals, compared to the examples given in the book.
164 | - not entirely sure that video is the right medium for getting these kinds of concepts across, but up for trying.
165 |
166 |
167 |
168 | # Competition:
169 |
170 | > What books or online resources compete with this book? Please list the title and author. In each case, how will your book be different or better in timing, content, coverage, approach, or tone?
171 |
172 |
173 |
174 | DDD book?
175 | patterns of enteprise arch?
176 |
177 |
178 |
179 | # Book Outline (old):
180 |
181 | 1. the simple approach
182 | 2. domain model
183 | 3. dependency injection and inversion of control
184 | 4. persistence and unit of work
185 | 5. commands & events, handlers, message bus
186 | 6. (ports and) adapters
187 | 7. command-query separation.
188 | 8. the proof is in the pudding 1: implementing a new feature
189 | 9. the proof is in the pudding 2: refactoring an infrastructure layer
190 | 10. comparison with the simple approach
191 | 11. conclusions: when to use these patterns
192 | A. appendix: functional core, imperative shell
193 | B. appendix: more design patterns
194 | C. alternatives
195 |
196 |
197 | # Specs and Schedule:
198 |
199 | > How many pages do you expect the book to be?
200 | 200-250?
201 |
202 | > What use you will make of illustrations or screenshots? Approximately how many illustrations do you anticipate using?
203 |
204 | Several code listings per chapter, and maybe a few diagrams which we can produce ourselves...
205 |
206 | > What special considerations apply to your plans for the book, including unusual format, use of color, hard-to-get illustrations, or anything else calling for unusual resources?
207 |
208 | nothing extraordinary is anticipated
209 |
210 | > When do you anticipate delivering a complete draft of the manuscript or technical review?
211 |
212 | 9 months' time?
213 |
214 |
--------------------------------------------------------------------------------
/push-branches.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import subprocess
4 | from pathlib import Path
5 | from chapters import CHAPTERS, NO_EXERCISE, STANDALONE
6 |
7 | processes = []
8 |
9 | for chapter in CHAPTERS + STANDALONE:
10 | print('pushing', chapter)
11 | processes.append(subprocess.Popen(
12 | ['git', 'push', '-v', '--force-with-lease', 'origin', chapter],
13 | cwd=Path(__file__).parent / 'code',
14 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
15 | ))
16 | if chapter in NO_EXERCISE:
17 | continue
18 | exercise_branch = f'{chapter}_exercise'
19 | print('pushing', exercise_branch)
20 | processes.append(subprocess.Popen(
21 | ['git', 'push', '-v', '--force-with-lease', 'origin', exercise_branch],
22 | cwd=Path(__file__).parent / 'code',
23 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
24 | ))
25 |
26 | print('pushing master')
27 | processes.append(subprocess.Popen(
28 | ['git', 'push', '-v', '--force-with-lease', 'origin', 'master'],
29 | cwd=Path(__file__).parent / 'code',
30 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
31 | ))
32 |
33 | for p in processes:
34 | stdout, stderr = p.communicate()
35 | print(stdout)
36 | print(stderr)
37 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = --tb=short
3 |
--------------------------------------------------------------------------------
/rebase-appendices.sh:
--------------------------------------------------------------------------------
1 | #/bin/bash
2 | set -ex
3 |
4 | cd code
5 | git co appendix_django
6 | git irebase chapter_06_uow
7 |
8 | git co appendix_csvs
9 | git irebase chapter_06_uow
10 |
11 | git co master
12 |
--------------------------------------------------------------------------------
/rebase-chapters.sh:
--------------------------------------------------------------------------------
1 | #/bin/bash
2 | set -ex
3 |
4 | if [[ $# -eq 0 ]] ; then
5 | echo 'need commit to rebase from'
6 | exit 0
7 | fi
8 |
9 | cd code
10 | git co chapter_13_dependency_injection
11 | git irebase $1
12 |
13 | git co master
14 | git reset --hard chapter_13_dependency_injection
15 |
16 | cd ..
17 |
--------------------------------------------------------------------------------
/render-diagrams.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import re
3 | import sys
4 | import tempfile
5 | import subprocess
6 | from pathlib import Path
7 | from lxml import html
8 |
9 | IMAGES_DIR = Path(__file__).absolute().parent / 'images'
10 |
11 |
12 | def all_chapter_names():
13 | for fn in sorted(Path(__file__).absolute().parent.glob('*.html')):
14 | chapter_name = fn.name.replace('.html', '')
15 | if chapter_name == 'book':
16 | continue
17 | yield chapter_name
18 |
19 | def main(paths):
20 | print(paths)
21 | if paths:
22 | chapter_names = [p.replace('.html', '').replace('.asciidoc', '') for p in paths]
23 | else:
24 | chapter_names = all_chapter_names()
25 | for chapter_name in chapter_names:
26 | render_images(chapter_name)
27 |
28 |
29 | def render_images(chapter_name):
30 | print('Rendering images for', chapter_name)
31 | raw_contents = Path(f'{chapter_name}.html').read_text()
32 | parsed_html = html.fromstring(raw_contents)
33 |
34 | for image_block in parsed_html.cssselect('.imageblock'):
35 | [img] = image_block.cssselect('img')
36 | image_id = img.get('src').replace('images/', '').replace('.png', '')
37 | print(image_id)
38 |
39 | parent = image_block.getparent()
40 | next_sibling_pos = parent.index(image_block) + 1
41 | try:
42 | next_element = parent[next_sibling_pos]
43 | except IndexError:
44 | continue
45 | if 'image-source' in next_element.classes:
46 | code = next_element.cssselect('pre')[0].text
47 | render_image(code, image_id)
48 |
49 | INCLUDES = [
50 | 'images/C4_Context.puml',
51 | 'images/C4_Component.puml',
52 | ]
53 |
54 | def _add_dots(source, image_id):
55 | lines = source.splitlines()
56 | assert lines[0].startswith('[')
57 | assert image_id in lines[0]
58 | plantuml_cfg = str(Path('plantuml.cfg').absolute())
59 | lines[0] = lines[0].replace('config=plantuml.cfg', f'config={plantuml_cfg}')
60 | lines[0] = re.sub(r'\[ditaa, (\w+)\]', r'[ditaa, \1, scale=4]', lines[0])
61 | for ix, l in enumerate(lines):
62 | if include := next((i for i in INCLUDES if i in l), None):
63 | lines[ix] = l.replace(include, str(Path(include).absolute()))
64 | lines.insert(1, '....')
65 | lines.append('....')
66 | return '\n'.join(lines)
67 |
68 |
69 | def render_image(source, image_id):
70 | source = _add_dots(source, image_id)
71 | print(source)
72 | target = Path(f'images/{image_id}.png')
73 | if target.exists():
74 | target.unlink()
75 | tf = Path(tempfile.NamedTemporaryFile().name)
76 | tf.write_text(source)
77 | cmd = ['asciidoctor', '-r', 'asciidoctor-diagram', '-a', f'imagesoutdir={IMAGES_DIR}', str(tf)]
78 | print(' '.join(cmd))
79 | subprocess.run(cmd, check=True)
80 |
81 |
82 | if __name__ == '__main__':
83 | main(sys.argv[1:])
84 |
--------------------------------------------------------------------------------
/renumber-chapters.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import subprocess
3 | from pathlib import Path
4 |
5 | MOVES = [
6 | # change these as desired
7 | ('chapter_04b_high_gear_low_gear', 'chapter_05_high_gear_low_gear'),
8 | ('chapter_05_uow', 'chapter_06_uow'),
9 | ('chapter_06_aggregate', 'chapter_07_aggregate'),
10 | ("chapter_07_events_and_message_bus", "chapter_08_events_and_message_bus"),
11 | ("chapter_08_all_messagebus", "chapter_09_all_messagebus"),
12 | ("chapter_09_commands", "chapter_10_commands"),
13 | ("chapter_10_external_events", "chapter_11_external_events"),
14 | ("chapter_11_cqrs", "chapter_12_cqrs"),
15 | ("chapter_12_dependency_injection", "chapter_13_dependency_injection"),
16 | ]
17 |
18 | for frm, to in MOVES:
19 | subprocess.run(['git', 'mv', f'{frm}.asciidoc', f'{to}.asciidoc'], check=True)
20 |
21 | sources = list(Path(__file__).absolute().parent.glob('*.asciidoc'))
22 | otherthings = [
23 | 'chapters.py',
24 | 'atlas.json',
25 | 'Readme.md',
26 | 'rebase-chapters.sh',
27 | 'rebase-appendices.sh',
28 | ]
29 | for frm, to in MOVES:
30 | subprocess.run(
31 | ['sed', '-i', f's/{frm}/{to}/g'] + sources + otherthings,
32 | check=True,
33 | )
34 |
35 | input('base repo done, ready to do submodules')
36 |
37 | for frm, to in MOVES:
38 | code = Path(__file__).absolute().parent / 'code'
39 | subprocess.run(['git', 'branch', '-m', frm, to], cwd=code)
40 | subprocess.run(['git', 'branch', '--unset-upstream', to], cwd=code) # untested
41 | subprocess.run(['git', 'push', 'origin', f':{frm}'], cwd=code)
42 | subprocess.run(['git', 'checkout', to], cwd=code)
43 | subprocess.run(['git', 'push', '-u', 'origin', to], cwd=code)
44 | from chapters import NO_EXERCISE
45 | if to not in NO_EXERCISE:
46 | # untested
47 | subprocess.run(
48 | ['git', 'branch', '-m', f'{frm}_exercise', f'{to}_exercise'],
49 | cwd=code, check=True,
50 | )
51 |
52 | input(f'{frm}->{to} done in theory')
53 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | lxml
2 | cssselect
3 | pytest-icdiff
4 | pylint
5 | mypy
6 | -r code/requirements.txt
7 |
--------------------------------------------------------------------------------
/reset-exercise-branches.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import subprocess
3 | from pathlib import Path
4 | from chapters import CHAPTERS
5 |
6 |
7 | def run(cmds):
8 | print(' '.join(cmds))
9 | p = subprocess.run(
10 | cmds,
11 | cwd=Path(__file__).parent / 'code',
12 | capture_output=True,
13 | )
14 | if p.returncode:
15 | raise Exception(p.stderr.decode())
16 | output = p.stdout.decode()
17 | print(output)
18 | return output
19 |
20 |
21 | all_branches = run(['git', 'branch', '-a'],)
22 |
23 | for chapter in CHAPTERS:
24 | exercise_chapter = f'{chapter}_exercise'
25 | if exercise_chapter not in all_branches:
26 | continue
27 | run(['git', 'checkout', exercise_chapter])
28 | run(['git', 'reset', '--hard', f'origin/{exercise_chapter}'])
29 | run(['git', 'checkout', 'master'])
30 |
--------------------------------------------------------------------------------
/tests.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=redefined-outer-name
2 | import re
3 | import subprocess
4 | from contextlib import contextmanager
5 | from dataclasses import dataclass
6 | from pathlib import Path
7 | from lxml import html
8 | import pytest
9 | from chapters import CHAPTERS, BRANCHES, STANDALONE, NO_EXERCISE
10 |
11 |
12 |
13 | def all_branches():
14 | return subprocess.run(
15 | ['git', 'branch', '-a'],
16 | cwd=Path(__file__).parent / 'code',
17 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
18 | check=True
19 | ).stdout.decode().split()
20 |
21 | def git_log(chapter):
22 | return subprocess.run(
23 | ['git', 'log', chapter, '--oneline', '--decorate'],
24 | cwd=Path(__file__).parent / 'code',
25 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
26 | check=True
27 | ).stdout.decode()
28 |
29 | @pytest.fixture(scope='session')
30 | def master_log():
31 | return git_log('master')
32 |
33 | @pytest.mark.parametrize('chapter', CHAPTERS)
34 | def test_master_has_all_chapters_in_its_history(master_log, chapter):
35 | if chapter in BRANCHES:
36 | return
37 | assert f'{chapter})' in master_log
38 |
39 | @pytest.mark.parametrize('chapter', CHAPTERS)
40 | def test_exercises_for_reader(chapter):
41 | exercise_branch = f'{chapter}_exercise'
42 | branches = all_branches()
43 | if chapter in NO_EXERCISE:
44 | if exercise_branch in branches:
45 | pytest.fail(f'looks like there is an exercise for {chapter} after all!')
46 | else:
47 | pytest.xfail(f'{chapter} has no exercise yet')
48 | return
49 | assert exercise_branch in branches
50 | assert f'{chapter})' in git_log(exercise_branch), f'Exercise for {chapter} not up to date'
51 |
52 | def previous_chapter(chapter):
53 | chapter_no = CHAPTERS.index(chapter)
54 | if chapter_no == 0:
55 | return None
56 | previous = CHAPTERS[chapter_no - 1]
57 | if previous in BRANCHES:
58 | previous = CHAPTERS[chapter_no - 2]
59 | return previous
60 |
61 | @pytest.mark.parametrize('chapter', CHAPTERS)
62 | def test_each_chapter_follows_the_last(chapter):
63 | previous = previous_chapter(chapter)
64 | if previous is None:
65 | return
66 | assert f'{previous})' in git_log(chapter), f'{chapter} did not follow {previous}'
67 |
68 |
69 | @pytest.mark.parametrize('chapter', CHAPTERS + STANDALONE)
70 | def test_chapter(chapter):
71 | for listing in parse_listings(chapter):
72 | check_listing(listing, chapter)
73 |
74 |
75 | @contextmanager
76 | def checked_out(chapter):
77 | subprocess.run(
78 | ['git', 'checkout', f'{chapter}'],
79 | cwd=Path(__file__).parent / 'code',
80 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
81 | check=True
82 | )
83 | try:
84 | yield
85 |
86 | finally:
87 | subprocess.run(
88 | ['git', 'checkout', '-'],
89 | cwd=Path(__file__).parent / 'code',
90 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
91 | check=True
92 | )
93 |
94 |
95 | def tree_for_branch(chapter_name):
96 | with checked_out(chapter_name):
97 | return subprocess.run(
98 | ['tree', '-v', '-I', '__pycache__|*.egg-info'],
99 | cwd=Path(__file__).parent / 'code',
100 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
101 | check=True
102 | ).stdout.decode()
103 |
104 |
105 | def check_listing(listing, chapter):
106 | if 'tree' in listing.classes:
107 | actual_contents = tree_for_branch(chapter)
108 | elif 'non-head' in listing.classes:
109 | actual_contents = file_contents_for_tag(
110 | listing.filename, chapter, listing.tag,
111 | )
112 | elif 'existing' in listing.classes:
113 | actual_contents = file_contents_for_previous_chapter(
114 | listing.filename, chapter,
115 | )
116 | elif listing.is_diff:
117 | actual_contents = diff_for_tag(
118 | listing.filename, chapter, listing.tag,
119 | )
120 |
121 | else:
122 | actual_contents = file_contents_for_branch(listing.filename, chapter)
123 | actual_lines = actual_contents.split('\n')
124 |
125 | if '...' in listing.contents:
126 | for section in re.split(r'#?\.\.\.', listing.fixed_contents):
127 | lines = section.splitlines()
128 | if section.strip() not in actual_contents:
129 | assert lines == actual_lines, \
130 | f'section from [{listing.tag}] not found within actual'
131 |
132 | elif listing.fixed_contents not in actual_contents:
133 | assert listing.lines == actual_lines, \
134 | f'listing [{listing.tag}] not found within actual'
135 |
136 |
137 |
138 | @dataclass
139 | class Listing:
140 | filename: str
141 | tag: str
142 | contents: str
143 | classes: list
144 | is_diff: bool
145 |
146 | callouts = re.compile(r' #?(\(\d\) ?)+$', flags=re.MULTILINE)
147 | callouts_alone = re.compile(r'^\(\d\)$')
148 |
149 | @property
150 | def fixed_contents(self):
151 | fixed = self.contents
152 | fixed = self.callouts.sub('', fixed)
153 | fixed = '\n'.join(
154 | l for l in fixed.splitlines()
155 | if not self.callouts_alone.match(l)
156 | )
157 | return fixed
158 |
159 | @property
160 | def lines(self):
161 | return self.fixed_contents.split('\n')
162 |
163 |
164 | def parse_listings(chapter_name):
165 | raw_contents = Path(f'{chapter_name}.html').read_text()
166 | parsed_html = html.fromstring(raw_contents)
167 |
168 | for listing_node in parsed_html.cssselect('.exampleblock'):
169 | [block_node] = listing_node.cssselect('.listingblock')
170 | classes = block_node.get('class').split()
171 | if 'skip' in classes:
172 | continue
173 |
174 | if 'tree' in classes:
175 | filename = None
176 | else:
177 | [title_node] = listing_node.cssselect('.title')
178 | title = title_node.text_content()
179 | print('found listing', title)
180 | try:
181 | filename = re.search(r'.+ \((.+)\)', title).group(1)
182 | except AttributeError as e:
183 | raise AssertionError(f'Could not find filename in title {title}') from e
184 |
185 | is_diff = bool(listing_node.cssselect('code[data-lang="diff"]'))
186 | tag = listing_node.get('id')
187 |
188 | [code_node] = block_node.cssselect('.content pre')
189 | yield Listing(
190 | filename, tag, contents=code_node.text_content(), classes=classes,
191 | is_diff=is_diff
192 | )
193 |
194 |
195 | def file_contents_for_branch(filename, chapter_name):
196 | return subprocess.run(
197 | ['git', 'show', f'{chapter_name}:{filename}'],
198 | cwd=Path(__file__).parent / 'code',
199 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
200 | check=True
201 | ).stdout.decode()
202 |
203 | def file_contents_for_previous_chapter(filename, chapter_name):
204 | previous = previous_chapter(chapter_name)
205 | return file_contents_for_branch(filename, previous)
206 |
207 | def file_contents_for_tag(filename, chapter_name, tag):
208 | output = subprocess.run(
209 | ['git', 'show', f'{chapter_name}^{{/\\[{tag}\\]}}:{filename}'],
210 | cwd=Path(__file__).parent / 'code',
211 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
212 | check=True
213 | ).stdout.decode()
214 | assert output.strip(), f'no commit found for [{tag}]'
215 | return output
216 |
217 | def diff_for_tag(filename, chapter_name, tag):
218 | if tag.endswith('_diff'):
219 | tag = tag[:-5]
220 | output = subprocess.run(
221 | ['git', 'show', f'{chapter_name}^{{/\\[{tag}\\]}}', '--', filename],
222 | cwd=Path(__file__).parent / 'code',
223 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
224 | check=True
225 | ).stdout.decode()
226 | assert output.strip(), f'no commit found for [{tag}]'
227 | return '\n'.join(l.rstrip() for l in output.splitlines())
228 |
--------------------------------------------------------------------------------
/theme/asciidoctor-clean.custom.css:
--------------------------------------------------------------------------------
1 | /* Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */
2 |
3 | @import url("//fonts.googleapis.com/css?family=Noto+Sans:300,600italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700");
4 | @import url(//asciidoctor.org/stylesheets/asciidoctor.css); /* Default asciidoc style framework - important */
5 |
6 | /* customisations by harry */
7 |
8 | h1, h2, h3, h4, h5, h6 {
9 | position: relative;
10 | }
11 |
12 | a.anchor {
13 | top: 0;
14 | }
15 |
16 | /* hide inline ditaa/plantuml source listings for images */
17 | .image-source {
18 | display: none
19 | }
20 | /* make formal codeblocks a bit nicer */
21 | .exampleblock > .content {
22 | padding: 2px;
23 | background-color: white;
24 | border: 0;
25 | margin-bottom: 2em;
26 | }
27 | .exampleblock .title {
28 | text-align: right;
29 | }
30 |
31 | /* prev/next chapter links at bottom of page */
32 | .prev_and_next_chapter_links {
33 | margin: 10px;
34 | }
35 | .prev_chapter_link {
36 | float: left;
37 | }
38 | .next_chapter_link {
39 | float: right;
40 | }
41 |
42 |
43 | /* a few tweaks to existing styles */
44 | #toc li {
45 | margin-top: 0.5em;
46 | }
47 |
48 | #footnotes hr {
49 | width: 100%;
50 | }
51 |
52 | /* end customisations by harry */
53 |
54 |
55 | /* CUSTOMISATIONS */
56 |
57 | /* Change the values in root for quick customisation. If you want even more fine grain... venture further. */
58 |
59 | :root{
60 | --maincolor:#FFFFFF;
61 | --primarycolor:#2c3e50;
62 | --secondarycolor:#ba3925;
63 | --tertiarycolor: #186d7a;
64 | --sidebarbackground:#CCC;
65 | --linkcolor:#b71c1c;
66 | --linkcoloralternate:#f44336;
67 | --white:#FFFFFF;
68 | --black:#000000;
69 | }
70 |
71 | /* Text styles */
72 | h1{color:var(--primarycolor) !important;}
73 |
74 | h2,h3,h4,h5,h6{color:var(--secondarycolor) !important;}
75 |
76 | .title{color:var(--tertiarycolor) !important; font-family:"Noto Sans",sans-serif !important;font-style: normal !important; font-weight: normal !important;}
77 | p{font-family: "Noto Sans",sans-serif !important}
78 |
79 | /* Table styles */
80 | th{font-family: "Noto Sans",sans-serif !important}
81 |
82 | /* Responsiveness fixes */
83 | video {
84 | max-width: 100%;
85 | }
86 |
87 | @media all and (max-width: 600px) {
88 | table {
89 | width: 55vw!important;
90 | font-size: 3vw;
91 | }
92 |
--------------------------------------------------------------------------------
/theme/epub/epub.css:
--------------------------------------------------------------------------------
1 | /* harry rule to hide inline image sources */
2 | .image-source {
3 | display: none !important;
4 | }
5 |
6 | /* Custom widths */
7 | .width-10 { width: 10% !important; }
8 | .width-20 { width: 20% !important; }
9 | .width-30 { width: 30% !important; }
10 | .width-40 { width: 40% !important; }
11 | .width-50 { width: 50% !important; }
12 | .width-60 { width: 60% !important; }
13 | .width-70 { width: 70% !important; }
14 | .width-75 { width: 75% !important; }
15 | .width-80 { width: 80% !important; }
16 | .width-90 { width: 90% !important; }
17 | .width-full,
18 | .width-100 { width: 100% !important; }
19 |
--------------------------------------------------------------------------------
/theme/epub/epub.xsl:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
25 |
26 | E
27 |
28 |
29 | P
30 | I
31 |
32 |
33 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/theme/pdf/pdf.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | /*--------Put Your Custom CSS Rules Below--------*/
4 | /* Globally preventing code blocks from breaking across pages
5 | pre { page-break-inside: avoid; } */
6 |
7 | /* Reduce font size (STYL-1219) */
8 | aside.small { font-size: 0.7em !important; }
9 |
10 | /* handling for elements to keep them from breaking across pages */
11 | .nobreakinside { page-break-inside: avoid; }
12 |
13 | /* Epilogue figure label */
14 | section[data-type="afterword"] figcaption:before {
15 | content: "Figure E-"counter(FigureNumber)". ";
16 | }
17 |
18 | /*less space for pagebreaks */
19 | .less_space {margin-top: 0 !important;}
20 |
21 | /* Allow Examples to have less space at top of page (STYL-1266)
22 | section.less_space > h1:first-child {
23 | margin-top: 0 !important;
24 |
25 | }
26 |
27 | aside.less_space > h5:first-child {
28 | margin-top: 0 !important;
29 |
30 | }
31 |
32 | .less_space > h5:first-child {
33 | margin-top: 0 !important;
34 | padding-top: 0 !important;
35 | }
36 | */
37 | /* Temporary fix to TOC spacing from Tools */
38 | nav[data-type="toc"] li {
39 | margin-bottom: 0 !important;
40 | }
41 |
42 | /* harry rule to hide inline image sources */
43 | .image-source {
44 | display: none !important;
45 | }
46 |
47 | /* Removing "Example X" labels from formal code block captions */
48 | section[data-type="chapter"] div[data-type="example"] h5:before {
49 | content: none;
50 | }
51 |
52 | section[data-type="appendix"] div[data-type="example"] h5:before {
53 | content: none;
54 | }
55 |
56 | section[data-type="preface"] div[data-type="example"] h5:before {
57 | content: none;
58 | }
59 |
60 | div[data-type="part"] div[data-type="example"] h5:before {
61 | content: none;
62 | }
63 |
64 | div[data-type="part"] section[data-type="chapter"] div[data-type="example"] h5:before {
65 | content: none;
66 | }
67 |
68 | div[data-type="part"] section[data-type="appendix"] div[data-type="example"] h5:before {
69 | content: none;
70 | }
71 |
72 | div[data-type="example"] h5 {
73 | text-align: right !important;
74 | font-size: 9pt !important;
75 | margin-bottom: 0.5ex !important;
76 | }
77 | /*--- This oneoff overrides the code in https://github.com/oreillymedia//blob/master/pdf/pdf.css---*/
78 |
79 | figure div.border-box { border: none; }
80 |
81 | /*----Uncomment to temporarily turn on code-eyballer highlighting (make sure to recomment after you build)
82 |
83 | pre {
84 | background-color: yellow;
85 | }
86 | ---*/
87 |
88 | /*----Uncomment to turn on automatic code wrapping
89 |
90 | pre {
91 | white-space: pre-wrap;
92 | word-wrap: break-word;
93 | }
94 | ----*/
95 |
96 | /*----Uncomment to change the TOC start page (set
97 | the number to one page _after_ the one you want;
98 | so 6 to start on v, 8 to start on vii, etc.)
99 |
100 | @page toc:first {
101 | counter-reset: page 6;
102 | }
103 | ----*/
104 |
105 | /*----Uncomment to fix a bad break in the title
106 | (increase padding value to push down, decrease
107 | value to pull up)
108 |
109 | section[data-type="titlepage"] h1 {
110 | padding-left: 1.5in;
111 | }
112 | ----*/
113 |
114 | /*----Uncomment to fix a bad break in the subtitle
115 | (increase padding value to push down, decrease
116 | value to pull up)
117 |
118 | section[data-type="titlepage"] h2 {
119 | padding-left: 1in;
120 | }
121 | ----*/
122 |
123 | /*----Uncomment to fix a bad break in the author names
124 | (increase padding value to push down, decrease
125 | value to pull up)
126 |
127 | section[data-type="titlepage"] p.author {
128 | padding-left: 3in;
129 | }
130 | ----*/
131 |
132 | /* ----Uncomment to suppress duplicate page numbers in index entries
133 | WARNING: MAY CAUSE PDF BUILDS TO SEGFAULT
134 |
135 | div[data-type="index"] {
136 | -ah-suppress-duplicate-page-number: true;
137 | }
138 |
139 | ----*/
140 |
--------------------------------------------------------------------------------
/theme/pdf/pdf.xsl:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
63 |
64 | E
65 |
66 |
67 | P
68 | I
69 |
70 |
71 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/titlepage.html:
--------------------------------------------------------------------------------
1 |
2 |
Architecture Patterns with Python
3 |
4 |
5 |
Enabling Test-Driven Development, Domain-Driven Design, and Event-Driven Microservices
6 |
7 |
Harry Percival and Bob Gregory
8 |
9 |
11 |
--------------------------------------------------------------------------------
/toc.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/tools/figure_renaming_report.tsv:
--------------------------------------------------------------------------------
1 | Original names New names
2 | images/big_ball_of_yarn.jpg images/apwp_0001.png
3 | images/layered_architecture.png images/apwp_0002.png
4 | images/part1_components_diagram.png images/apwp_p101.png
5 | images/maps_chapter_01_notext.png images/apwp_0101.png
6 | images/allocation_context_diagram.png images/apwp_0102.png
7 | images/model_diagram.png images/apwp_0103.png
8 | images/maps_chapter_01_withtext.png images/apwp_0104.png
9 | images/maps_chapter_02.png images/apwp_0201.png
10 | images/layered_architecture.png images/apwp_0202.png
11 | images/onion_architecture.png images/apwp_0203.png
12 | images/model_diagram.png images/apwp_0204.png
13 | images/repository_pattern_diagram.png images/apwp_0205.png
14 | images/domain_model_tradeoffs_diagram.png images/apwp_0206.png
15 | images/coupling_illustration1.png images/apwp_0301.png
16 | images/coupling_illustration2.png images/apwp_0302.png
17 | images/maps_chapter_04_before.png images/apwp_0401.png
18 | images/maps_chapter_04_after.png images/apwp_0402.png
19 | images/service_layer_diagram_abstract_dependencies.png images/apwp_0403.png
20 | images/service_layer_diagram_test_dependencies.png images/apwp_0404.png
21 | images/service_layer_diagram_runtime_dependencies.png images/apwp_0405.png
22 | images/test_spectrum_diagram.png images/apwp_0501.png
23 | images/maps_chapter_05_before.png images/apwp_0601.png
24 | images/maps_chapter_05_after.png images/apwp_0602.png
25 | images/maps_chapter_06.png images/apwp_0701.png
26 | images/before_aggregates_diagram.png images/apwp_0702.png
27 | images/after_aggregates_diagram.png images/apwp_0703.png
28 | images/version_numbers_sequence_diagram.png images/apwp_0704.png
29 | images/allocation_context_diagram.png images/apwp_p201.png
30 | images/maps_chapter_07.png images/apwp_0801.png
31 | images/maps_chapter_08_before.png images/apwp_0901.png
32 | images/maps_chapter_08_after.png images/apwp_0902.png
33 | images/batch_changed_events_flow_diagram.png images/apwp_0903.png
34 | images/reallocation_sequence_diagram.png images/apwp_0904.png
35 | images/maps_chapter_10.png images/apwp_1101.png
36 | images/batches_context_diagram.png images/apwp_1102.png
37 | images/command_flow_diagram_1.png images/apwp_1103.png
38 | images/command_flow_diagram_2.png images/apwp_1104.png
39 | images/command_flow_diagram_with_error.png images/apwp_1105.png
40 | images/reallocation_sequence_diagram_with_redis.png images/apwp_1106.png
41 | images/maps_chapter_11.png images/apwp_1201.png
42 | images/read_model_sequence_diagram.png images/apwp_1202.png
43 | images/maps_chapter_12_before.png images/apwp_1301.png
44 | images/maps_chapter_12_after.png images/apwp_1302.png
45 | images/maps_chapter_12_after.png images/apwp_aa01.png
46 |
--------------------------------------------------------------------------------
/tools/intakereport.txt:
--------------------------------------------------------------------------------
1 | Title: Architecture Patterns with Python
2 | ISBN: 9781492052203
3 | JIRA #: DCPSPROD-3883
4 |
5 |
6 | Stylesheet: animal_theme_sass
7 | Toolchain: Atlas 2
8 |
9 | Atlas URL: https://atlas.oreilly.com/oreillymedia/architecture-patterns-with-python
10 |
11 | Incoming format: Asciidoc
12 | Outgoing format: Asciidoc
13 |
14 | Preliminary pagecount: 292
15 |
16 | Is this project in Early Release? YES
17 |
18 | Resources
19 | =========
20 |
21 | ** Figs: Illustrations is still working on the figs.
22 | 44 total. (0 are informal; 465 are inline.)
23 |
24 | Once the figs are processed on /work, you'll need to add them to the book's repo.
25 |
26 | A report mapping original figure file names to their new names can be found in the tools folder for this project as figure_renaming_report.tsv.
27 |
28 | ** Intake Report:
29 | (Git repo) tools/intakereport.txt
30 |
31 | ** MS Snapshot:
32 | To view the submitted files, you'll need to checkout the git tag named 'manuscript_to_prod'
33 | by running the following command in your checkout:
34 |
35 | $ git checkout manuscript_to_prod
36 |
37 | This will temporarily switch the files in your repo to the state they were in when the manuscript_to_prod tag
38 | was created.
39 | To switch the files back to the current state, run:
40 |
41 | $ git checkout master
42 |
43 |
44 | Notes from Tools:
45 | =================
46 |
47 | ** PROD: Add any authors to project that need to be added.
48 | ** Syntax highlighting: applied to 218 out of 264 code listings.
49 |
50 | ** Note that the URL of the repo has been changed to match the title.
51 | ** The errors related to code callouts stem from two callouts that are missing their corresponding list items. These are located at chapter_13_dependency_injection.asciidoc line 689 and appendix_validation.asciidoc line 405. Please have the author advise on whether to remove the callout or add the missing list item.
52 | ** As noted below, there are some customizations I made to the Examples. Please see TOOLSREQ-4934 for details.
53 |
54 | ** Please let Tools know ASAP if there are any other global problems for which we can help automate a fix.
55 |
56 |
57 | Notes at arrival in production:
58 | ==================
59 |
60 | Notes for Production:
61 | * They are hoping to have print copies to sign at the Pycon Conference April 15-23, 2020. That's a little before the ERD, so if extra effort can be made to let this happen, it would be great. [KB added padding to the schedule to account for your honeymoon, and we have an early March FTP. If we can hold that and make this a March ERD instead of April ERD, big hurrah!]
62 | * Both authors live in the UK, so you may experience delays in communication. Harry functions as the lead and responds far more often than Bob. Confirm with them, but usually an okay from Harry is good enough for approval. Make sure he agrees and knows to rouse Bob on anything he wants Bob to specifically approve. [KB has worked with Harry before, he's super nice!]
63 | * Figure log is at https://docs.google.com/spreadsheets/d/1xg0EhcmcSklv9WxqyFbB0Kvcsj9lPGxr/edit#gid=29876908.
64 | * All figures can be used as-is as far as we can tell. No need for redraws. [KB sez: these do look fine, though slight inconsistencies in style. Some of them have light blue text that may have legibility issues, so going to ask Rebecca to darken them.]
65 | * Authors using Examples but want the "Example X-X" bits hidden. Tools has modified the asciidoc for this project to remove the "Example X-X" from appearing, so maybe production doesn't have to do anything on this. They also have the example titles right-aligned, which is intentional (you can ask KB about Harry Percival's other book and the formatting quirks, if you're curious).
66 | * Watch for use of singular "I" – author voice should be plural "we"
67 | * Watch for sentence style headings; use ORM heading styles
68 | * Take a light hand in the edit, unless stuff is just wrong. If in doubt, best to query. They have some quirky ideas about writing, and I gave them more than the usual leeway. They needed some "wins" since we imposed the book's title against their will and really put the hammer down in terms of schedule. They are also proud of UK English and may try to sneak some in, including using pounds instead of dollars. I recommend letting a lot of that go if it seems insignificant.
69 | * Tools: in epubcheck, I see some errors related to callout list icons. [Nick: See Notes from Tools]
70 | * Some duplicate hardcoded part numbers (Part I: Part 1: Title) [Nick: I fixed the Part titles]
71 |
72 | ==================
73 |
74 | Please let me know about any other issues.
75 |
76 | Thanks,
77 | -Nick
78 |
79 |
--------------------------------------------------------------------------------
/travis-deploy-key.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fushall/cosmicpython-book/6326789352febf2f2b578d191aafb2ce24d9024a/travis-deploy-key.enc
--------------------------------------------------------------------------------
/update-exercise-branch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import sys
3 | import subprocess
4 | from pathlib import Path
5 |
6 |
7 | def run(cmds):
8 | print(' '.join(cmds))
9 | p = subprocess.run(
10 | cmds,
11 | cwd=Path(__file__).parent / 'code',
12 | capture_output=True,
13 | check=True
14 | )
15 | if p.returncode:
16 | raise Exception(p.stderr.decode())
17 | output = p.stdout.decode()
18 | print(output)
19 | return output
20 |
21 | all_branches = run(['git', 'branch', '-a'],)
22 |
23 |
24 | def main(chapter):
25 | exercise_chapter = f'{chapter}_exercise'
26 | assert exercise_chapter in all_branches
27 |
28 | run(['git', 'checkout', exercise_chapter])
29 | commits = list(reversed(run([
30 | 'git', 'log', '--pretty=%h',
31 | f'{exercise_chapter}^{{/{chapter}_ends}}..{exercise_chapter}',
32 | ]).split()))
33 | run(['git', 'reset', '--hard', chapter])
34 | run(['git', 'cherry-pick', *commits])
35 | run(['git', 'checkout', 'master'])
36 |
37 | if __name__ == '__main__':
38 | main(sys.argv[1])
39 |
--------------------------------------------------------------------------------
/uppercase-titles.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import re
3 | from pathlib import Path
4 | from titlecase import titlecase
5 | from chapters import CHAPTERS
6 |
7 | SPECIAL = {
8 | 'CSVs', 'To/From', 'UoW', 'AKA', 'SELECT',
9 | }
10 |
11 | LOWERCASE = {
12 | 'with',
13 | }
14 |
15 |
16 |
17 | def specialcases(w, **_):
18 | if w.lower() in LOWERCASE:
19 | return w.lower()
20 | if w.lower() in {s.lower() for s in SPECIAL}:
21 | return next(special for special in SPECIAL if special.lower() == w.lower())
22 | if '_' in w:
23 | return w
24 | return None
25 |
26 |
27 | def fix_line(l):
28 | if not l.startswith('=='):
29 | return l
30 | if l == '====':
31 | return l
32 | if l == '.':
33 | return l
34 | prefix, rest = re.match(r'(=+ |\.)(.+)', l).groups()
35 | return prefix + titlecase(rest, callback=specialcases)
36 |
37 |
38 | def main():
39 | for chapter in CHAPTERS:
40 | path = Path(f'{chapter}.asciidoc')
41 | contents = path.read_text()
42 | fixed = '\n'.join(
43 | fix_line(l) for l in contents.splitlines()
44 | )
45 | path.write_text(fixed)
46 |
47 | if __name__ == '__main__':
48 | main()
49 |
50 | import pytest
51 |
52 | def test_lowercases_short_words():
53 | assert fix_line('=== What Is A Domain Model') == '=== What Is a Domain Model'
54 |
55 | def test_fix_line_handles_quotes_and_slashes():
56 | assert fix_line('=== Foo "bar" baz') == '=== Foo "Bar" Baz'
57 |
58 | def test_fix_line_leaves_small_words_alone_except_at_beginning():
59 | assert fix_line('=== This is a line') == '=== This Is a Line'
60 | assert fix_line('=== The initial the is fine') == '=== The Initial the Is Fine'
61 |
62 | @pytest.mark.skip
63 | def test_dotstarters():
64 | assert fix_line('.A sidebar title') == '.A Sidebar Title'
65 |
66 | def test_hyphens():
67 | assert fix_line('=== A wrap-up') == '=== A Wrap-Up'
68 |
69 | def test_uow():
70 | assert fix_line('=== A Uow') == '=== A UoW'
71 |
72 | def test_underscores():
73 | assert fix_line('=== A special_method') == '=== A special_method'
74 |
--------------------------------------------------------------------------------