|
| 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 |
0 commit comments