Slide 1

Slide 1 text

Clean Architecture Clean Architecture Sebastian Buczyński Sebastian Buczyński

Slide 2

Slide 2 text

conference tourist developer in STX Next Łódź runs of Python Łódź meetup blogs under breadcrumbscollector.tech

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

complexity complexity

Slide 5

Slide 5 text

Two types of complexity Two types of complexity accidental essen al No Silver Bullet – Essence and Accident in So ware Engineering

Slide 6

Slide 6 text

Clean Architecture Clean Architecture 1. Independence of frameworks 2. Testability 3. Independence of UI or database

Slide 7

Slide 7 text

Clean Architecture Clean Architecture Separates complexity of your code

Slide 8

Slide 8 text

Project: Auctions online Project: Auctions online

Slide 9

Slide 9 text

User stories User stories As a bidder I want to make a bid to win an auc on As a bidder I want to be no fied by e‑mail when my bid is a winning one As an administrator I want to be able to withdraw a bid

Slide 10

Slide 10 text

Django + Rest Framework! Django + Rest Framework!

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

User stories -> code User stories -> code As a bidder I want to make a bid to win an auc on As a bidder I want to be no fied by e‑mail when my bid is a winning one As an administrator I want to be able to withdraw a bid

Slide 13

Slide 13 text

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 14

Slide 14 text

User stories User stories As a bidder I want to make a bid to win an auc on ✔ As a bidder I want to be no fied by e‑mail when my offer is a winning one ✔ As an administrator I want to be able to withdraw a bid

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

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 old_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 17

Slide 17 text

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 old_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 18

Slide 18 text

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 old_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 19

Slide 19 text

No content

Slide 20

Slide 20 text

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) old_winners = set(auction.winners) auction.withdraw_bids(bids_to_withdraw) new_winners = set(auction.winners) self._notify_winners(new_winners - old_winners)

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

What about tests?! What about tests?! Business logic is coupled with a framework, so are tests...

Slide 23

Slide 23 text

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 24

Slide 24 text

No content

Slide 25

Slide 25 text

How much time wasted, exactly? How much time wasted, exactly? h ps:/ /breadcrumbscollector.tech/is‑your‑test‑suite‑was ng‑your‑ me/

Slide 26

Slide 26 text

How a textbook example looks like? How a textbook example looks like? No side effects and dependencies makes code easier to test PURE FUNCTION class MyTest(unittest.TestCase): def test_add(self): expected = 7 actual = add(3, 4) self.assertEqual(actual, expected)

Slide 27

Slide 27 text

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) old_winners = set(auction.winners) auction.withdraw_bids(bids_to_withdraw) new_winners = set(auction.winners) self._notify_winners(new_winners - old_winners)

Slide 28

Slide 28 text

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) old_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 29

Slide 29 text

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) old_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 30

Slide 30 text

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 31

Slide 31 text

No content

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

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 36

Slide 36 text

Clean Arch - building block #3 Clean Arch - building block #3 Interface Adapter / Adapter class DjangoAuctionsRepo(AuctionsRepo): def get(self, auction_id): return Auction.objects.get(pk=auction_id)

Slide 37

Slide 37 text

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 38

Slide 38 text

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 39

Slide 39 text

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 deferred OOP done right

Slide 40

Slide 40 text

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) old_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 41

Slide 41 text

Clean Arch - building block #0 Clean Arch - building block #0 En ty 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 42

Slide 42 text

Clean Arch - building block #3 Clean Arch - building block #3 Interface Adapter / 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 43

Slide 43 text

Entity vs model #1 Entity vs model #1 En ty = data & rules ‑ adhere to Tell, don't ask principle auction = Auction(id=1, title='Super auction', bids=[]) auction.bids.append(Bid()) ✘ auction.make_a_bid(Bid()) ✓

Slide 44

Slide 44 text

Entity vs model #2 Entity vs model #2 En ty can represent graph of objects

Slide 45

Slide 45 text

En ty Interface / Port Interface Adapter / Adapter Use Case / Interactor Presenter* + space for more *see exemplary project Clean Arch building blocks altogether Clean Arch building blocks altogether

Slide 46

Slide 46 text

Clean Arch building blocks altogether Clean Arch building blocks altogether

Slide 47

Slide 47 text

Clean Arch building blocks altogether Clean Arch building blocks altogether

Slide 48

Slide 48 text

Clean Arch building blocks altogether Clean Arch building blocks altogether

Slide 49

Slide 49 text

You MUST NOT use/import anything from a layer above! Clean Arch building blocks altogether Clean Arch building blocks altogether

Slide 50

Slide 50 text

Boundary Clean Arch building blocks altogether Clean Arch building blocks altogether

Slide 51

Slide 51 text

What to be careful of? What to be careful of?

Slide 52

Slide 52 text

non-idiomatic framework use non-idiomatic framework use

Slide 53

Slide 53 text

Word on frameworks Word on frameworks ✓ Pyramid ✓ Flask ✘ Django* *if you like pain

Slide 54

Slide 54 text

more code (type hints help) more code (type hints help)

Slide 55

Slide 55 text

copying data between objects copying data between objects

Slide 56

Slide 56 text

validation? validation?

Slide 57

Slide 57 text

DRF serializers, colander, marshmallow, DRF serializers, colander, marshmallow, type checking type checking

Slide 58

Slide 58 text

value objects value objects money = Decimal('10.00') # meh

Slide 59

Slide 59 text

value objects value objects money = Money('10.00012') # raises ValueError money = Money('10.12$') # yay!

Slide 60

Slide 60 text

overengineering overengineering

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

No content

Slide 63

Slide 63 text

When it pays off? When it pays off?

Slide 64

Slide 64 text

lots of cases - testability lots of cases - testability

Slide 65

Slide 65 text

Testing entities Testing entities def test_should_use_initial_price_as_current_price_when_no_bids() auction = create_auction() assert auction.current_price == auction.initial_price def test_should_return_highest_bid_amount_for_current_price(): auction = create_auction(bids=[ Bid(id=1, bidder_id=1, amount=Decimal('20')), Bid(id=2, bidder_id=2, amount=Decimal('15')), ]) assert auction.current_price == Decimal('20')

Slide 66

Slide 66 text

Testing use cases Testing use cases def test_saves_auction( auctions_repo_mock: Mock, auction_mock: Mock, input_dto: PlacingBidInputDto ) -> None: PlacingBidUseCase().execute(input_dto) auctions_repo_mock.save.assert_called_once_with(auction_mock)

Slide 67

Slide 67 text

deferring decision making - stay lean deferring decision making - stay lean

Slide 68

Slide 68 text

complicated domain complicated domain

Slide 69

Slide 69 text

Futher reading Futher reading h ps:/ /8thlight.com/blog/uncle‑bob/2012/08/13/the‑clean‑architecture.html Clean Architecture: A Cra sman's Guide to So ware Structure and Design Clean Architecture Python (web) apps ‑ Przemek Lewandowski So ware 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) Czysta Architektura: Jak stworzyć testowalny i elastyczny kod (justjoin.it)

Slide 70

Slide 70 text

I'm wri ng a book! cleanarchitecture.io

Slide 71

Slide 71 text

That's all, folks! That's all, folks! Questions? Questions? cleanarchitecture.io/talk | | breadcrumbscollector.tech @EnforcerPL