-
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathddd-toolkit.html
More file actions
140 lines (119 loc) · 10.4 KB
/
Copy pathddd-toolkit.html
File metadata and controls
140 lines (119 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<!doctype html><html><head><meta charset="utf-8"><meta name="description" content="Domain Driven Design Toolkit"><meta name="author" content="Artem Malyshev"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Domain Driven Design Toolkit</title><link href="reveal.css" rel="stylesheet"></head><body><div class="reveal"><div class="slides"><section><h2>Domain Driven Design Toolkit</h2><h4><a href="https://github.com/proofit404">Artem Malyshev</a></h4><h6><a href="https://twitter.com/proofit404">@proofit404</a></h6></section><section><h2>BIO</h2><ul><li>Co-Founder at <a href="https://drylabs.io/">drylabs.io</a></li><li><a href="https://dry-python.org/">dry-python.org</a></li><li>Django Channels 1.0</li><li>5 years of experience in Python</li></ul></section><section><h2>Complexity</h2><p class="fragment"><b>Accidental complexity</b> refers to challenges that developers unintentionally make for themselves as a result of trying to solve a problem.</p><p class="fragment"><b>Essential complexity</b> is just the nature of the beast you're trying to tame.</p></section><section data-background-image="08efb3c614f6ee7d9d59f11a92d2f814.jpg" data-background-size="contain"></section><section data-background-image="c52a9db5b7fd8a1a5cc9a819fbcea5c9.jpg" data-background-size="contain"></section><section><h2>Accidental complexity</h2><ul><li class="fragment">AsyncIO vs. Gevent</li><li class="fragment">PostgreSQL vs. MongoDB</li><li class="fragment">Python vs. Go</li><li class="fragment">Emacs vs. Vim</li><li class="fragment">Tabs vs. Spaces</li></ul></section><section><img class="plain" src="0f97676130bd6321766a03b91159f7b1.jpg"></section><section><h2>What is the domain-driven design?</h2><p class="fragment">Focus on the core complexity and opportunity in the domain</p><p class="fragment">Explore models in a collaboration of domain experts and software experts</p><p class="fragment">Write software that expresses those models explicitly</p><p class="fragment">Speak <b>ubiquitous language</b> within a <b>bounded context</b></p></section><section><h2>Not about technology</h2><p class="fragment">You have to master the tool first then you can focus on <b>DDD</b>.</p><p class="fragment">In most domain models, most design patterns are technical noise.</p></section><section><img class="plain" src="c86fdc0d1c0abfb93f1263ec8a0a7f09.jpg"></section><section data-background-image="7342db757b86c2aceb1a6b33d4ddce96.jpg" data-background-size="contain"></section><section data-background-image="07fb3115c562bce7922a441f21b2754f.jpg" data-background-size="contain"></section><section><h2>What is a model?</h2><p class="fragment"><b>HINT:</b> Not a UML diagram</p><p class="fragment">A set of services, entities and value objects expressed in classes, methods, and refs.</p></section><section><h2><a href="https://dry-python.org/">dry-python</a></h2><p>A set of libraries for pluggable business logic components.</p><img class="plain" src="https://raw.githubusercontent.com/dry-python/brand/master/logo/dry-python.png"></section><section data-background-image="ebc6682574f1147cb5360827813df21f.jpg" data-background-size="contain"></section><section><img class="plain" src="https://raw.githubusercontent.com/dry-python/brand/master/logo/stories.png"><p>Define a user story in the business transaction DSL.</p><p>Separate state, implementation and specification.</p></section><section><h2>Layout</h2><pre><code>project
├── admin
├── forms
├── migrations
├── models
├── <b>services</b>
├── templates
└── views</code></pre></section><section><h2>Specification DSL</h2><pre><code class="python">from stories import story, arguments
class Subscription:
@story
@arguments("invoice_id", "user")
def buy(I):
I.find_order
I.find_price
I.find_invoice
I.check_balance
I.persist_payment
I.persist_subscription
I.send_subscription_notification</code></pre></section><section><h2>State Contract</h2><pre><code class="python">from pydantic import BaseModel
class Subscription:
@story
def buy(I):
...
@Subscription.buy.contract
class Context(BaseModel):
user: User
invoice_id: int
invoice: Optional[Invoice]</code></pre></section><section><h2>Steps implementation</h2><pre><code class="python">from stories import Failure, Success
class Subscription:
def find_invoice(self, ctx: Context):
invoice = Invoice.objects.get(pk=ctx.invoice_id)
return Success(invoice=invoice)
def check_balance(self, ctx: Context):
if ctx.user.can_pay(ctx.invoice):
return Success()
else:
return Failure()</code></pre></section><section><h2>Story Execution</h2><pre><code class="python">>>> Subscription().buy(category_id=2)
Subscription.buy:
find_category
check_price
check_purchase (PromoCode.validate)
find_code (skipped)
check_balance
find_profile
Context:
category_id = 1318 # Story argument
user = <User: 3292> # Story argument
category = <Category: 1318>
# Set by Subscription.find_category</code></pre></section><section data-background-image="41a288079292c670fc6adbb991c81b5d.png" data-background-size="contain"><h2>DEBUG TOOLBAR</h2><br><br><br><br><br><br><br><br><br><br></section><section data-background-image="2112306ed16594557ec48c5aadcb9840.png" data-background-size="contain"><h2 style="color: white">py.test</h2></section><section data-background-image="ea36d933df7211c5a711818f2145d767.png" data-background-size="contain"><h2>Sentry</h2></section><section data-background-image="c7436da758bca93b26c96ebec943822b.gif" data-background-size="contain"><h2>ELK</h2></section><section data-background-image="fe2a6f1002015746d4defff9ebec738d.jpg" data-background-size="contain"></section><section data-background-image="1fa2c566de60b024fde7730f19297048.jpg" data-background-size="contain"></section><section data-background-image="ef28087b0403552a5cdf87fa0f38b05a.jpg" data-background-size="contain"></section><section data-background-image="1918b59f4cd7b5f9debfe2f9d52c9f86.jpg" data-background-size="contain"></section><section data-background-image="2bb69726eb46f44c13f8fe7d61f9ed22.jpg" data-background-size="contain"></section><section data-background-image="e8302875a09798293cc3d2c49dcf6fdb.jpg" data-background-size="contain"></section><section><h2>Layout</h2><pre><code>project
├── admin
├── <b>aggregates</b>
├── forms
├── migrations
├── models
├── services
├── templates
└── views</code></pre></section><section><h2>dataclasses</h2><pre><code class="python">from dataclasses import dataclass
from typing import List, NewType
OrderId = NewType("OrderId", int)
@dataclass
class LineItem:
product_id: ProductId
@dataclass
class Order:
primary_key: OrderId
items: List[LineItem]</code></pre></section><section data-background-image="127ce80f030ec2d257e80f1a89c43991.jpg" data-background-size="contain"></section><section><img class="plain" src="https://raw.githubusercontent.com/dry-python/brand/master/logo/mappers.png"><p>Declarative mappers from ORM models to domain entities. And back again!</p></section><section><h2>Layout</h2><pre><code>project
├── admin
├── aggregates
├── forms
├── migrations
├── models
├── <b>repositories</b>
├── services
├── templates
└── views</code></pre></section><section><h2>Django ORM</h2><pre><code class="python">from mappers import Mapper
from app.aggregates import Order, OrderId, User
from app.models import OrderModel, UserModel
mapper = Mapper(Order, OrderModel, {"primary_key": "pk"})
@mapper.reader
def load_order(id: OrderId, user: User) -> Order:
friends = UserModel.objects.filter(
purchases=OuterRef("pk"), friends=user.primary_key)
return OrderModel.objects.filter(pk=id).annotate(
purchased_by_friends=Exists(friends)).get()</code></pre></section><section><h2>Swagger definitions</h2><pre><code class="python">from mappers import Mapper
from bravado import swagger_model
from app.aggregates import Price
spec = swagger_model.load_file("price_service.yml")
mapper = Mapper(Price, spec.definitions["Price"])
@mapper.reader
def load_price(id: PriceId) -> Price:
return requests.get(f"http://172.16.1.7/get/{id}")</code></pre></section><section><h2>GraphQL queries</h2><pre><code class="python">from mappers import Mapper
from gql import gql, Client, build_schema
from app.models import Invoice
schema = build_schema("invoice_service.graphql")
mapper = Mapper(Invoice, schema.get_type_map()["Invoice"])
@mapper.reader
def load_invoice(id: InvoiceId) -> Invoice:
return Client(schema=schema).execute(gql("""
{
loadInvoice(id: %(id)d)
}
""", {"id": id}))</code></pre></section><section data-background-image="c783ff4507677d561a1134e96eabee52.png" data-background-size="contain"></section><section><h2>Tests & Mocks</h2><pre><code class="python">def test_before(monkeypatch):
monkeypatch.setattr(pusher, "Pusher", Mock())
# ...
pusher.Pusher.trigger.assert_called_once_with(
"private-user-1"
)
def test_after(emitter):
# ...
emitter.trigger.assert_called_once_with(
UserStream(User(primary_key=1))
)</code></pre></section><section><img class="plain" src="https://raw.githubusercontent.com/dry-python/brand/master/logo/dependencies.png"><p>Provide composition instead of inheritance.</p><p>Solves top-down approach problem.</p></section><section><h2>Dependency Injection</h2><pre><code class="python">from dependencies import Injector, Package
app = Package("project")
class BuySubscription(Injector):
buy_subscription = app.services.Subscription.buy
load_order = app.repositories.load_order
load_price = app.repositories.load_price
load_invoice = app.repositories.load_invoice
BuySubscription.buy_subscription(category_id=1, price_id=1)</code></pre></section><section><h2>Refactoring roadmap</h2><ul><li class="fragment">No DDD at all</li><li class="fragment">Stories without contracts</li><li class="fragment">Contracts and aggregates</li><li class="fragment">Mappers</li><li class="fragment">Dependency injection</li></ul></section><section data-background-image="43006fbd632816d1077938b4abe8ddd2.png" data-background-size="contain"></section><section><h2>Questions?</h2></section></div></div><script src="reveal.js"></script></body></html>