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

December 27, 2018
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

  1. CODE IS... CODE IS... hard and frustrating Let's consider we're

    developing subscription button for a web service 3
  2. 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
  3. BIG FRAMEWORK BIG FRAMEWORK 1. You need method flowchart 2.

    Zig-zag in the traceback 3. Framework internals leak 8
  4. 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
  5. 10

  6. 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
  7. AS A RESULT CODE IS... AS A RESULT CODE IS...

    1. Fragile 2. Hard to reason about 3. Time-consuming 12
  8. 13

  9. 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
  10. 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
  11. 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
  12. 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 = <User: 3292> # Story argument category = <Category: 1318> # Set by Subscription.find_category 19
  13. 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)
  14. 23

  15. 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
  16. 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
  17. 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
  18. 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('<h1>Not enough money</h1>') 28
  19. 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 '<h1>Not enough money</h1>' 29
  20. 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
  21. 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