Keeping Software Soft

Artem Malyshev

@proofit404

BIO




pros

  1. Relatively easy to read

cons

  1. Really hard to change
  2. We can not see the whole picture

Implicit API

class Purchases(viewsets.ModelViewSet):
    queryset = Purchase.objects.all()
    serializer_class = PurchaseSerializer
    permission_classes = (CanPurchase,)
    filter_class = PurchaseFilter
router.register('/api/purchases/', Purchases)
  1. What exactly does this class do?
  2. How to use it?

Function to change

from google_cloud_messaging import send_message

@observer
def send_sms(event):
    text = 'You purchase something!'
    on_commit(lambda: send_message(text))

pros

  1. Relatively easy to change

cons

  1. Really hard to read
  2. You should keep in mind framework rules
  3. Implicit knowledge grow
  4. We still can not see the whole picture

Business-friendly tools

Scenario: Publishing the article
  Given I am an author user
  And I have an article
  When I go to the article page
@given('I am an author user')
def author_user(ctx):
    ctx['user'] = Author()

@given('I have an article')
def article(ctx):
    ctx['article'] = create_article(author=ctx['user'])

@when('I go to the article page')
def go_to_article(ctx):
    Browser().visit(f"/articles/{ctx['article'].id}/")

pros

  1. Clean flow in the source code
  2. Separate step implementation
  3. Each step knows nothing about a neighbor
  4. Easy reuse of code

cons

  1. Does not relate to programming so much

dry-python

A set of libraries for pluggable business logic components.

Define a user story in the business transaction DSL.

Separate state, implementation and specification.

Specification DSL

from stories import story, arguments

class Purchase:
    @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_purchase
        I.send_purchase_notification

Steps implementation

from stories import Failure, Success

class Purchase:
    # ...

    def find_invoice(self, ctx):
        invoice = Invoice.objects.get(pk=ctx.invoice_id)
        return Success(invoice=invoice)

    def check_balance(self, ctx):
        if ctx.user.can_pay(ctx.invoice):
            return Success()
        else:
            return Failure()

Problems

We do not have the tooling to work with data

There are no data contracts written in code

dataclasses

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]

State Contract

from pydantic import BaseModel

class Purchase:
    @story
    def buy(I):
        ...

@Purchase.buy.contract
class Context(BaseModel):
    user: User
    invoice_id: InvoiceId
    invoice: Optional[Invoice]

pros

  1. Explicit data contracts and relations in code
  2. Data store independent
  3. Catch errors when they occur
  4. Not when they propagate to exception

cons

  1. Working with data sources manually

Declarative mappers from ORM models to domain entities. And back again!

Django ORM

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()

Swagger definitions

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}")

GraphQL queries

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}))

How to use third-party library

from pusher import Pusher

class Purchase:
    def send_purchase_notification(ctx):
        Pusher().trigger("private-user-1")
def test_before(monkeypatch):
    monkeypatch.setattr(pusher, "Pusher", Mock())
    # ...
    pusher.Pusher.trigger.assert_called_once_with(
        "private-user-1"
    )

How to use it with DI

class Purchase:
    def send_purchase_notification(ctx):
        self.trigger_message(UserStream(ctx.user))

    trigger_message: Emitter
def test_after(emitter):
    # ...
    Purchase.trigger_message.assert_called_once_with(
        UserStream(User(primary_key=1))
    )

DEBUG TOOLBAR











py.test

Sentry

ELK