Upgrade to Pro — share decks privately, control downloads, hide ads and more …

dry-python: расширяемая архитектура из коробки

dry-python: расширяемая архитектура из коробки

Артём Малышев (Self employed) @ MoscowPython Meetup 62

"Как часто, получая новый ticket, вы задумываетесь: "Ну и где тебя искать?" Как часто, вглядываясь в обработчик запроса, вы гадали: "Что тут вообще творится?" Качественный код всегда тяжело проектировать в начале, а ценить вложенные усилия начинаешь спустя время. В своём докладе я раскрою нехитрые подходы, которые позволят упростить дальнейшую жизнь проектов. А так же покажу проект dry-python, воплотивший эти подходы в виде нескольких библиотек".

Видео: http://www.moscowpython.ru/meetup/62/dry-python/

Moscow Python Meetup
PRO

December 27, 2018
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

  1. MAKING ARCHITECTURE MATTER
    MAKING ARCHITECTURE MATTER
    ARTEM MALYSHEV
    ARTEM MALYSHEV
    DRY-PYTHON.ORG
    DRY-PYTHON.ORG
    1

    View Slide

  2. BIO
    BIO
    5 years experience in Python
    Django Channels 1.0
    Dry Python
    2

    View Slide

  3. CODE IS...
    CODE IS...
    hard
    and frustrating
    Let's consider we're developing subscription button
    for a web service
    3

    View Slide

  4. STARTUP
    STARTUP
    4

    View Slide

  5. MICRO FRAMEWORK
    MICRO FRAMEWORK
    1. Long handlers
    2. Lots of "if" statements
    5

    View Slide

  6. LONG HANDLERS
    LONG HANDLERS
    85 @app.route('/subscriptions/')
    86 def buy_subscription(page):
    ...
    121 if props[-1].endswith('$'):
    122 -> props[-1] = props[-1][:-1]
    123
    Traceback (most recent call last):
    File "views.py", line 1027, in buy_subscription
    ZeroDivisionError: division by zero
    6

    View Slide

  7. ENTERPRISE
    ENTERPRISE
    7

    View Slide

  8. BIG FRAMEWORK
    BIG FRAMEWORK
    1. You need method flowchart
    2. Zig-zag in the traceback
    3. Framework internals leak
    8

    View Slide

  9. IMPLICIT API
    IMPLICIT API
    1. What exactly does this class do?
    2. How to use it?
    class SubscriptionViewSet(viewsets.ModelViewSet):
    queryset = Subscription.objects.all()
    serializer_class = SubscriptionSerializer
    permission_classes = (CanSubscribe,)
    filter_class = SubscriptionFilter
    9

    View Slide

  10. 10

    View Slide

  11. FRAMEWORK INTERNALS LEAK
    FRAMEWORK INTERNALS LEAK
    class SubscriptionSerializer(Serializer):
    category_id = IntegerField()
    price_id = IntegerField()
    def recreate_nested_writable_fields(self, instance):
    for field, values in self.writable_fields_to_recreate():
    related_manager = getattr(instance, field)
    related_manager.all().delete()
    for data in values:
    obj = related_manager.model.objects.create(
    to=instance, **data)
    related_manager.add(obj)
    11

    View Slide

  12. AS A RESULT CODE IS...
    AS A RESULT CODE IS...
    1. Fragile
    2. Hard to reason about
    3. Time-consuming
    12

    View Slide

  13. 13

    View Slide

  14. If your code is crap, stickies on the wall
    won't help.
    @HenrikKniberg
    14

    View Slide

  15. SERVICE LAYER
    SERVICE LAYER
    Defines an application's boundary with a layer of
    services that establishes a set of available operations
    and coordinates the application's response in each
    operation.
    by Randy Stafford
    15

    View Slide

  16. BUSINESS OBJECTS
    BUSINESS OBJECTS
    def buy_subscription(category_id, price_id, user):
    category = find_category(category_id)
    price = find_price(price_id)
    profile = find_profile(user)
    if profile.balance < price.cost:
    raise ValueError
    decrease_balance(profile, price.cost)
    save_profile(profile)
    expires = calculate_period(price.period)
    subscription = create_subscription(
    profile, category, expires)
    notification = send_notification(
    'subscription', profile, category.name)
    16

    View Slide

  17. DRY PYTHON
    DRY PYTHON
    A set of libraries for pluggable business logic
    components.
    17

    View Slide

  18. from stories import story, arguments
    class Subscription:
    @story
    @arguments('category_id', 'price_id')
    def buy(I):
    I.find_category
    I.find_price
    I.find_profile
    I.check_balance
    I.persist_payment
    I.persist_subscription
    I.send_subscription_notification
    18

    View Slide

  19. CONTEXT
    CONTEXT
    (Pdb) ctx
    Subscription.buy:
    find_category
    check_price
    check_purchase (PromoCode.validate)
    find_code (skipped)
    check_balance
    find_profile
    Context:
    category_id = 1318 # Story argument
    user = # Story argument
    category =
    # Set by Subscription.find_category
    19

    View Slide

  20. DEBUG TOOLBAR
    DEBUG TOOLBAR
    20

    View Slide

  21. PY.TEST
    PY.TEST
    21

    View Slide

  22. SENTRY
    SENTRY
    22

    View Slide

  23. USAGE
    USAGE
    1. Story decorator build an execution plan
    2. Execute object methods according to plan
    3. We call the story method
    class Subscription:
    @story
    def buy(I):
    I.find_category
    def find_category(self, ctx):
    category = Category.objects.get(
    pk=ctx.category_id)
    return Success(category=category)
    subs = Subscription()
    subs.buy(category_id=1, price_id=1)

    View Slide

  24. 23

    View Slide

  25. USAGE
    USAGE
    1. Steps can be stories as well
    2. Failure will stop the execution of the whole story
    class Subscription:
    @story
    def buy(I):
    I.check_purchase
    @story
    def check_purchase(I):
    I.find_promo_code
    def check_balance(self, ctx):
    if ctx.profile.balance < ctx.price.cost:
    return Failure()
    else:
    return Success()
    24

    View Slide

  26. Provide composition instead of inheritance.
    Use stories with different frameworks without
    boilerplate.
    25

    View Slide

  27. DELEGATE RESPONSIBILITY
    DELEGATE RESPONSIBILITY
    class Subscription:
    def find_category(self, ctx):
    category = self.load_category(ctx.category_id)
    return Success(category=category)
    def find_price(self, ctx):
    price = self.load_price(ctx.price_id)
    return Success(price=price)
    def __init__(self, load_category, load_price):
    self.load_category = load_category
    self.load_price = load_price
    26

    View Slide

  28. INJECTION
    INJECTION
    from dependencies import Injector, Package
    app = Package('app')
    class BuySubscription(Injector):
    buy_subscription = app.services.Subscription.buy
    load_category = app.repositories.load_category
    load_price = app.repositories.load_price
    load_profile = app.repositories.load_profile
    BuySubscription.buy_subscription(category_id=1, price_id=1)
    27

    View Slide

  29. DJANGO VIEWS
    DJANGO VIEWS
    from dependencies import operation
    from dependencies.contrib.django import view
    from django.http import HttpResponse, HttpResponseRedirect
    @view
    class BuySubscriptionView(BuySubscription):
    @operation
    def post(buy_subscription, category_id, price_id):
    result = buy_subscription.run(category_id, price_id)
    if result.is_success:
    return HttpResponseRedirect(to=result.value)
    elif result.failed_on('check_balance'):
    return HttpResponse('Not enough money')
    28

    View Slide

  30. FLASK VIEWS
    FLASK VIEWS
    from dependencies import operation
    from dependencies.contrib.flask import method_view
    from flask import redirect
    @method_view
    class BuySubscriptionView(BuySubscription):
    @operation
    def post(buy_subscription, category_id, price_id):
    result = buy_subscription.run(category_id, price_id)
    if result.is_success:
    return redirect(result.value)
    elif result.failed_on('check_balance'):
    return 'Not enough money'
    29

    View Slide

  31. CELERY TASKS
    CELERY TASKS
    from dependencies import operation
    from dependencies.contrib.celery import task
    @task
    class PutMoneyTask(PutMoney):
    @operation
    def run(put_money, user, amount, task):
    result = put_money.run(user, amount)
    if result.is_failure:
    task.on_failure(result.ctx.transaction_id)
    30

    View Slide

  32. PLANS
    PLANS
    1. Conditional substories
    2. Delegates
    3. Rollbacks
    4. asyncio support
    5. pyramid support
    6. typing advantages
    7. linters integration
    8. language server
    31

    View Slide

  33. TRY IT!
    TRY IT!
    $ pip install stories
    $ pip install dependencies
    32

    View Slide

  34. GET IN TOUCH
    GET IN TOUCH
    dry-python.org
    twitter.com/dry_py
    github.com/dry-python
    gitter.im/dry-python
    33

    View Slide