Skip to content

Commit 91c2fa9

Browse files
committed
Add second DDD toolkit talk for PyCon Russia.
1 parent f6541fc commit 91c2fa9

File tree

4 files changed

+345
-0
lines changed

4 files changed

+345
-0
lines changed

content/talks/contents.lr

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ title: Talks
22
---
33
body:
44

5+
### Domain Driven Design Toolkit 2
6+
7+
[Slides](/static/slides/ddd-toolkit-2.html)
8+
59
### Keeping software soft
610

711
<iframe width="1280" height="720" src="https://www.youtube.com/embed/yokuWjeZqfs" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

slides/ddd-toolkit-2.pug

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
doctype html
2+
html
3+
head
4+
meta(charset="utf-8")
5+
meta(name="description" content="Domain Driven Design Toolkit")
6+
meta(name="author" content="Artem Malyshev")
7+
meta(name="apple-mobile-web-app-capable" content="yes")
8+
meta(name="apple-mobile-web-app-status-bar-style" content="black-translucent")
9+
meta(name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no")
10+
title Domain Driven Design Toolkit
11+
body
12+
.reveal
13+
.slides
14+
section
15+
h2 Domain Driven Design Toolkit
16+
h4
17+
a(href="https://github.com/proofit404") Artem Malyshev
18+
h6
19+
a(href="https://twitter.com/proofit404") @proofit404
20+
img(src=require("./pics/pycon-logo.png") height="150").plain
21+
section
22+
h2 BIO
23+
ul
24+
li Co-Founder at #[a(href="https://drylabs.io/") drylabs.io]
25+
li #[a(href="https://dry-python.org/") dry-python.org]
26+
li Django Channels 1.0
27+
li 5 years of experience in Python
28+
section(data-background-image=require("./pics/startup-process.png") data-background-size="contain" data-background-color="#2E677B")
29+
section(data-background-image=require("./pics/long-function.jpg") data-background-size="contain")
30+
section
31+
h2 Implicit API
32+
pre
33+
code.python.
34+
class Purchases(viewsets.ModelViewSet):
35+
queryset = Purchase.objects.all()
36+
serializer_class = PurchaseSerializer
37+
permission_classes = (CanPurchase,)
38+
filter_class = PurchaseFilter
39+
pre
40+
code.python.
41+
router.register('/api/purchases/', Purchases)
42+
ol
43+
li.fragment What exactly does this class do?
44+
li.fragment How to use it?
45+
section
46+
h2 Complexity
47+
p.fragment #[b Accidental complexity] refers to challenges that developers unintentionally make for themselves as a result of trying to solve a problem.
48+
p.fragment #[b Essential complexity] is just the nature of the beast you're trying to tame.
49+
section
50+
h2 Accidental complexity
51+
ul
52+
li.fragment AsyncIO vs. Gevent
53+
li.fragment PostgreSQL vs. MongoDB
54+
li.fragment Python vs. Go
55+
li.fragment Emacs vs. Vim
56+
li.fragment Tabs vs. Spaces
57+
section(data-background-image=require("./pics/eric-evans.jpg") data-background-size="contain" data-background-position="left" data-background-color="#000000")
58+
section
59+
img(src=require("./pics/domain-driven-design.jpg")).plain
60+
section
61+
h2 What is the domain-driven design?
62+
p.fragment Focus on the core complexity and opportunity in the domain
63+
p.fragment Explore models in a collaboration of domain experts and software experts
64+
p.fragment Write software that expresses those models explicitly
65+
p.fragment Speak #[b ubiquitous language] within a #[b bounded context]
66+
section(data-background-image=require("./pics/ddd-schema.jpg") data-background-size="contain")
67+
section(data-background-image=require("./pics/ddd-schema-models-selected.jpg") data-background-size="contain")
68+
section
69+
h2 What is a model?
70+
p.fragment #[b HINT:] Not a UML diagram
71+
p.fragment A set of services, entities and value objects expressed in classes, methods, and refs.
72+
section
73+
h2
74+
a(href="https://dry-python.org/") dry-python
75+
p A set of libraries for pluggable business logic components.
76+
img(src="https://raw.githubusercontent.com/dry-python/brand/master/logo/dry-python.png").plain
77+
section(data-background-image=require("./pics/ddd-schema-services-selected.jpg") data-background-size="contain")
78+
section
79+
img(src="https://raw.githubusercontent.com/dry-python/brand/master/logo/stories.png").plain
80+
p Define a user story in the business transaction DSL.
81+
p Separate state, implementation and specification.
82+
section
83+
h2 Layout
84+
pre
85+
code.
86+
project
87+
├── admin
88+
├── forms
89+
├── migrations
90+
├── models
91+
├── #[b services]
92+
├── templates
93+
└── views
94+
section
95+
h2 Specification DSL
96+
pre
97+
code.python.
98+
from stories import story, arguments
99+
100+
class Subscription:
101+
@story
102+
@arguments("invoice_id", "user")
103+
def buy(I):
104+
I.find_order
105+
I.find_price
106+
I.find_invoice
107+
I.check_balance
108+
I.persist_payment
109+
I.persist_subscription
110+
I.send_subscription_notification
111+
section
112+
h2 Steps implementation
113+
pre
114+
code.python.
115+
from stories import Failure, Success
116+
117+
class Subscription:
118+
# ...
119+
120+
def find_invoice(self, ctx: Context):
121+
invoice = Invoice.objects.get(pk=ctx.invoice_id)
122+
return Success(invoice=invoice)
123+
124+
def check_balance(self, ctx: Context):
125+
if ctx.user.can_pay(ctx.invoice):
126+
return Success()
127+
else:
128+
return Failure()
129+
section
130+
h2 Story Execution
131+
pre
132+
code.python.
133+
>>> Subscription().buy(category_id=2)
134+
Subscription.buy:
135+
find_category
136+
check_price
137+
check_purchase (PromoCode.validate)
138+
find_code (skipped)
139+
check_balance
140+
find_profile
141+
142+
Context:
143+
category_id = 1318 # Story argument
144+
user = #{"<"}User: 3292#{">"} # Story argument
145+
category = #{"<"}Category: 1318#{">"}
146+
# Set by Subscription.find_category
147+
section
148+
h2 pros
149+
ol
150+
li.fragment Clean flow in the source code
151+
li.fragment Separate step implementation
152+
li.fragment Each step knows nothing about a neighbor
153+
li.fragment Easy reuse of code
154+
li.fragment Allows to instrument code easily
155+
section(data-background-image=require("./pics/debug-toolbar.png") data-background-size="contain")
156+
h2 DEBUG TOOLBAR
157+
br
158+
br
159+
br
160+
br
161+
br
162+
br
163+
br
164+
br
165+
br
166+
br
167+
section(data-background-image=require("./pics/pytest.png") data-background-size="contain")
168+
h2(style="color: white") py.test
169+
section(data-background-image=require("./pics/sentry.png") data-background-size="contain")
170+
h2 Sentry
171+
section(data-background-image=require("./pics/elk-logs-ui.gif") data-background-size="contain")
172+
h2 ELK
173+
section(data-background-image=require("./pics/ddd-schema-entities-selected.jpg") data-background-size="contain")
174+
section
175+
img(src=require("./pics/postgresql.png") height="150" style=" padding-right: 70px;").plain
176+
img(src=require("./pics/firebase.png") height="150" style=" padding-right: 70px;").plain
177+
ul
178+
li.fragment We do not have the tooling to work with data
179+
li.fragment There are no data contracts written in code
180+
section
181+
img(src=require("./pics/attrs.png")).plain
182+
section
183+
h2 Layout
184+
pre
185+
code.
186+
project
187+
├── admin
188+
├── #[b aggregates]
189+
├── forms
190+
├── migrations
191+
├── models
192+
├── services
193+
├── templates
194+
└── views
195+
section
196+
h2 dataclasses
197+
pre
198+
code.python.
199+
from dataclasses import dataclass
200+
from typing import List, NewType
201+
202+
OrderId = NewType("OrderId", int)
203+
204+
@dataclass
205+
class LineItem:
206+
product_id: ProductId
207+
208+
@dataclass
209+
class Order:
210+
primary_key: OrderId
211+
items: List[LineItem]
212+
section
213+
h2 State Contract
214+
pre
215+
code.python.
216+
from pydantic import BaseModel
217+
218+
class Purchase:
219+
@story
220+
def buy(I):
221+
# ...
222+
223+
@Purchase.buy.contract
224+
class Context(BaseModel):
225+
user: User
226+
invoice_id: InvoiceId
227+
invoice: Optional[Invoice]
228+
section
229+
h2 pros
230+
ol
231+
li.fragment Explicit data contracts and relations in code
232+
li.fragment Data store independent
233+
li.fragment Catch errors when they occur
234+
li.fragment Not when they propagate to exception
235+
h2.fragment cons
236+
ol
237+
li.fragment Working with data sources manually
238+
section(data-background-image=require("./pics/ddd-schema-repositories-selected.jpg") data-background-size="contain")
239+
section
240+
img(src="https://raw.githubusercontent.com/dry-python/brand/master/logo/mappers.png").plain
241+
p Declarative mappers from ORM models to domain entities. And back again!
242+
section
243+
h2 Django ORM
244+
pre
245+
code.python.
246+
from mappers import Mapper
247+
from app.aggregates import Order, OrderId, User
248+
from app.models import OrderModel, UserModel
249+
250+
mapper = Mapper(Order, OrderModel, {"primary_key": "pk"})
251+
252+
@mapper.reader
253+
def load_order(id: OrderId, user: User) -> Order:
254+
friends = UserModel.objects.filter(
255+
purchases=OuterRef("pk"), friends=user.primary_key)
256+
return OrderModel.objects.filter(pk=id).annotate(
257+
purchased_by_friends=Exists(friends)).get()
258+
section
259+
h2 Swagger definitions
260+
pre
261+
code.python.
262+
from mappers import Mapper
263+
from bravado import swagger_model
264+
from app.aggregates import Price
265+
266+
spec = swagger_model.load_file("price_service.yml")
267+
mapper = Mapper(Price, spec.definitions["Price"])
268+
269+
@mapper.reader
270+
def load_price(id: PriceId) -> Price:
271+
return requests.get(f"http://172.16.1.7/get/{id}")
272+
section
273+
h2 GraphQL queries
274+
pre
275+
code.python.
276+
from mappers import Mapper
277+
from gql import gql, Client, build_schema
278+
from app.aggregates import Invoice
279+
280+
schema = build_schema("invoice_service.graphql")
281+
mapper = Mapper(Invoice, schema.get_type_map()["Invoice"])
282+
283+
@mapper.reader
284+
def load_invoice(id: InvoiceId) -> Invoice:
285+
return Client(schema=schema).execute(gql("""
286+
{
287+
loadInvoice(id: %(id)d)
288+
}
289+
""", {"id": id}))
290+
section
291+
img(src=require("./pics/pusher.png")).plain
292+
section
293+
h2 How we use third-party libraries
294+
pre
295+
code.python.
296+
from pusher import Pusher
297+
298+
class Purchase:
299+
def send_purchase_notification(ctx):
300+
Pusher().trigger("private-user-1")
301+
pre
302+
code.python.
303+
def test_before(monkeypatch):
304+
monkeypatch.setattr(pusher, "Pusher", Mock())
305+
# ...
306+
pusher.Pusher.trigger.assert_called_once_with(
307+
"private-user-1"
308+
)
309+
section
310+
h2 How to use it with DI
311+
pre
312+
code.python.
313+
@dataclass
314+
class Purchase:
315+
def send_purchase_notification(ctx):
316+
self.trigger_message(UserStream(ctx.user))
317+
318+
trigger_message: Emitter
319+
pre
320+
code.python.
321+
def test_after(emitter):
322+
# ...
323+
Purchase.trigger_message.assert_called_once_with(
324+
UserStream(User(primary_key=1))
325+
)
326+
section(data-background-image=require("./pics/keep-calm-and-ddd.png") data-background-size="contain" data-background-color="#C10C06")
327+
section
328+
h2 Get in touch
329+
ul
330+
li
331+
a(href="https://dry-python.org/") dry-python.org
332+
li
333+
a(href="https://twitter.com/dry_py") twitter.com/dry_py
334+
li
335+
a(href="https://github.com/dry-python") github.com/dry-python
336+
li
337+
a(href="https://gitter.im/dry-python") gitter.im/dry-python

slides/pics/pycon-logo.png

311 KB
Loading

slides/webpack.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ module.exports = {
5252
template: 'keeping-software-soft.pug',
5353
filename: 'keeping-software-soft.html',
5454
}),
55+
new HtmlWebpackPlugin({
56+
template: 'ddd-toolkit-2.pug',
57+
filename: 'ddd-toolkit-2.html',
58+
}),
5559
new MiniCssExtractPlugin({
5660
filename: '[name].css',
5761
}),

0 commit comments

Comments
 (0)