Clean Architecture Clean Architecture in Python in Python - - PowerPoint PPT Presentation

clean architecture clean architecture
SMART_READER_LITE
LIVE PREVIEW

Clean Architecture Clean Architecture in Python in Python - - PowerPoint PPT Presentation

Clean Architecture Clean Architecture in Python in Python Sebastian Buczyski Sebastian Buczyski @ PyKonik Tech Talks #36 PyKonik Tech Talks #36 Clean Architecture Clean Architecture 1. Independence of frameworks 2. Testability 3.


slide-1
SLIDE 1

Clean Architecture Clean Architecture

in Python in Python

Sebastian Buczyński Sebastian Buczyński @ PyKonik Tech Talks #36 PyKonik Tech Talks #36

slide-2
SLIDE 2
slide-3
SLIDE 3
slide-4
SLIDE 4

Clean Architecture Clean Architecture

  • 1. Independence of frameworks
  • 2. Testability
  • 3. Independence of UI
  • 4. Independence of database
slide-5
SLIDE 5

Clean Architecture Clean Architecture Pung customer's concerns in the first place

slide-6
SLIDE 6

Project: Auctions online Project: Auctions online

slide-7
SLIDE 7

User stories User stories

As a bidder I want to make a bid to win an aucon As a bidder I want to be nofied by e‑mail when my offer is a winning one As an administrator I want to be able to withdraw a bid

slide-8
SLIDE 8

Django + Rest Framework! Django + Rest Framework!

slide-9
SLIDE 9
slide-10
SLIDE 10

Models first Models first

class Auction(models.Model): title = models.CharField(...) initial_price = models.DecimalField(...) current_price = models.DecimalField(...) class Bid(models.Model): amount = models.DecimalField(...) bidder = models.ForeignKey(...) auction = models.ForeignKey(Auction, on_delete=PROTECT)

slide-11
SLIDE 11

User stories User stories

As a bidder I want to make a bid to win an aucon ✔ As a bidder I want to be nofied by e‑mail when my offer is a winning one ✔ As an administrator I want to be able to withdraw a bid

slide-12
SLIDE 12

def save_related(self, request, form, formsets, *args, **kwargs): ids_of_deleted_bids = self._get_ids_of_deleted_bids(formsets) bids_to_withdraw = Bid.objects.filter( pk__in=ids_of_deleted_bids) auction = form.instance

  • ld_winners = set(auction.winners)

auction.withdraw_bids(bids_to_withdraw) new_winners = set(auction.winners) self._notify_winners(new_winners - old_winners) super().save_related(request, _form, formsets, *args, **kwarg

slide-13
SLIDE 13

def save_related(self, request, form, formsets, *args, **kwargs): ids_of_deleted_bids = self._get_ids_of_deleted_bids(formsets) bids_to_withdraw = Bid.objects.filter( pk__in=ids_of_deleted_bids) auction = form.instance

  • ld_winners = set(auction.winners)

auction.withdraw_bids(bids_to_withdraw) new_winners = set(auction.winners) self._notify_winners(new_winners - old_winners) super().save_related(request, _form, formsets, *args, **kwarg

slide-14
SLIDE 14

def save_related(self, request, form, formsets, *args, **kwargs): ids_of_deleted_bids = self._get_ids_of_deleted_bids(formsets) bids_to_withdraw = Bid.objects.filter( pk__in=ids_of_deleted_bids) auction = form.instance

  • ld_winners = set(auction.winners)

auction.withdraw_bids(bids_to_withdraw) new_winners = set(auction.winners) self._notify_winners(new_winners - old_winners) super().save_related(request, _form, formsets, *args, **kwarg

slide-15
SLIDE 15
slide-16
SLIDE 16
slide-17
SLIDE 17

Clean Arch - building block #1 Clean Arch - building block #1

UseCase OR Interactor

class WithdrawingBid: def withdraw_bids(self, auction_id, bids_ids): auction = Auction.objects.get(pk=auction_id) bids_to_withdraw = Bid.objects.filter( pk__in=ids_of_deleted_bids)

  • ld_winners = set(auction.winners)

auction.withdraw_bids(bids_to_withdraw) new_winners = set(auction.winners) self._notify_winners(new_winners - old_winners)

slide-18
SLIDE 18

UseCase - Orchestrates a particular process UseCase - Orchestrates a particular process

slide-19
SLIDE 19

What about tests?! What about tests?!

Business logic is coupled with a framework, so are tests...

slide-20
SLIDE 20

Testing through views Testing through views

from django.test import TestCase class LoginTestCase(TestCase): def test_login(self): User.objects.create(...) response = self.client.get('/dashboard/') self.assertRedirects(response, '/accounts/login/')

slide-21
SLIDE 21

How a textbook example looks like? How a textbook example looks like?

No side effects and dependencies makes code easier to test

class MyTest(unittest.TestCase): def test_add(self): expected = 7 actual = add(3, 4) self.assertEqual(actual, expected)

slide-22
SLIDE 22

Getting rid of dependencies: find them Getting rid of dependencies: find them

class WithdrawingBidUseCase: def withdraw_bids(self, auction_id, bids_ids): auction = Auction.objects.get(pk=auction_id) bids_to_withdraw = Bid.objects.filter( pk__in=ids_of_deleted_bids)

  • ld_winners = set(auction.winners)

auction.withdraw_bids(bids_to_withdraw) new_winners = set(auction.winners) self._notify_winners(new_winners - old_winners)

slide-23
SLIDE 23

Getting rid of dependencies: hide them Getting rid of dependencies: hide them

class WithdrawingBidUseCase: def withdraw_bids(self, auction_id, bids_ids): auction = self.auctions_repository.get(auction_id) bids = self.bids_repository.get_by_ids(bids_ids)

  • ld_winners = set(auction.winners)

auction.withdraw_bids(bids) new_winners = set(auction.winners) self.auctions_repository.save(auction) for bid in bids: self.bids_repository.save(bid) self._notify_winners(new_winners - old_winners)

slide-24
SLIDE 24

Getting rid of dependencies: hide them Getting rid of dependencies: hide them

class WithdrawingBidUseCase: def withdraw_bids(self, auction_id, bids_ids): auction = self.auctions_repository.get(auction_id) bids = self.bids_repository.get_by_ids(bids_ids)

  • ld_winners = set(auction.winners)

auction.withdraw_bids(bids) new_winners = set(auction.winners) self.auctions_repository.save(auction) for bid in bids: self.bids_repository.save(bid) self._notify_winners(new_winners - old_winners)

slide-25
SLIDE 25

Clean Arch - building block #2 Clean Arch - building block #2

Interface / Port

class AuctionsRepo(metaclass=ABCMeta): @abstractmethod def get(self, auction_id): pass @abstractmethod def save(self, auction): pass

slide-26
SLIDE 26

Clean Arch - building block #3 Clean Arch - building block #3

Interface Adapter / Port Adapter

class DjangoAuctionsRepo(AuctionsRepo): def get(self, auction_id): return Auction.objects.get(pk=auction_id)

slide-27
SLIDE 27

Combine together Combine together

class WithdrawingBidUseCase: def __init__(self, auctions_repository: AuctionsRepo): self.auctions_repository = auctions_repository django_adapter = DjangoAuctionsRepo() withdrawing_bid_uc = WithdrawingBidUseCase(django_adapter)

slide-28
SLIDE 28

Dependency Injection Dependency Injection

import inject def configure_inject(binder: inject.Binder): binder.bind(AuctionsRepo, DjangoAuctionsRepo()) inject.configure_once(configure_inject) class WithdrawingBidUseCase: auctions_repo: AuctionsRepo = inject.attr(AuctionsRepo)

slide-29
SLIDE 29

Benefits from another layer Benefits from another layer It is easier to reason about logic It is possible to write TRUE unit tests Work can be parallelized Decision making can be delayed

slide-30
SLIDE 30

Our logic is still coupled to a database! Our logic is still coupled to a database!

class WithdrawingBidUseCase: def withdraw_bids(self, auction_id, bids_ids): auction = self.auctions_repository.get(auction_id) bids = self.bids_repository.get_by_ids(bids_ids)

  • ld_winners = set(auction.winners)

auction.withdraw_bids(bids) new_winners = set(auction.winners) self.auctions_repository.save(auction) for bid in bids: self.bids_repository.save(bid) self._notify_winners(new_winners - old_winners)

slide-31
SLIDE 31

Clean Arch - building block #0 Clean Arch - building block #0

Enty

class Auction: def __init__(self, id: int, title: str, bids: List[Bid]): self.id = id self.title = title self.bids = bids def withdraw_bids(self, bids: List[Bid]): ... def make_a_bid(self, bid: Bid): ... @property def winners(self): ...

slide-32
SLIDE 32

Clean Arch - building block #3 Clean Arch - building block #3

Interface Adapter / Port Adapter

class DjangoAuctionsRepo(AuctionsRepo): def get(self, auction_id: int) -> Auction: auction_model = AuctionModel.objects.prefetch_related( 'bids' ).get(pk=auction_id) bids = [ self._bid_from_model(bid_model) for bid_model in auction_model.bids.all() ] return Auction( auction_model.id, auction_model.title, bids )

slide-33
SLIDE 33

All that's left is to call All that's left is to call UseCase from UseCase from Django Django any framework any framework

slide-34
SLIDE 34

Clean Arch building blocks altogether

slide-35
SLIDE 35

What to be careful of? What to be careful of? non‑idiomac framework use more code (type hints help) copying data between objects validaon?

  • verengineering
slide-36
SLIDE 36
slide-37
SLIDE 37
slide-38
SLIDE 38

When it pays off? When it pays off? lots of cases ‑ testability delaying decision making ‑ stay lean complicated domain

slide-39
SLIDE 39

That's all, folks! That's all, folks!

Questions? Questions?

slide-40
SLIDE 40

Futher reading Futher reading

hps:/ /8thlight.com/blog/uncle‑bob/2012/08/13/the‑clean‑architecture.html Clean Architecture: A Crasman's Guide to Soware Structure and Design Clean Architecture Python (web) apps ‑ Przemek Lewandowski Soware architecture chronicles ‑ blog posts series Boundaries ‑ Gary Bernhardt Exemplary project in PHP (blog post) Exemplary project in PHP (repo) Exemplary project in C# (repo) Exemplary project in Python (repo)

| | breadcrumbscollector.tech @EnforcerPL