Don't start with a database Practical clean architecture Who I am - - PowerPoint PPT Presentation

don t start with a database
SMART_READER_LITE
LIVE PREVIEW

Don't start with a database Practical clean architecture Who I am - - PowerPoint PPT Presentation

Don't start with a database Practical clean architecture Who I am Grzegorz Kocjan Goal of this talk P R O T I P # 0 New in Python Typing def get(order_id: int) -> dict: order_name: str = 'EuroPython2019!' order: dict = { 'id':


slide-1
SLIDE 1

Don't start with a database

Practical clean architecture

slide-2
SLIDE 2

Who I am

Grzegorz Kocjan

slide-3
SLIDE 3

Goal of this talk

slide-4
SLIDE 4

P R O T I P #

slide-5
SLIDE 5

New in Python

slide-6
SLIDE 6

Typing

def get(order_id: int) -> dict:

  • rder_name: str = 'EuroPython2019!'
  • rder: dict = {

'id': order_id, 'name': order_name } return order

slide-7
SLIDE 7

Typing

def get(order_id: int) -> dict:

  • rder_name: str = 'EuroPython2019!'
  • rder: dict = {

'id': order_id, 'name': order_name } return order

slide-8
SLIDE 8

Typing

def get(order_id: int) -> dict:

  • rder_name: str = 'EuroPython2019!'
  • rder: dict = {

'id': order_id, 'name': order_name } return order

P R O T I P # 1

slide-9
SLIDE 9

Typing

from typing import Dict, Union Order = Dict[str, Union[int,str]] def get(order_id: int) -> Order:

  • rder_name: str = 'EuroPython2019!'
  • rder: Order = {

'id': order_id, 'name': order_name } return order

slide-10
SLIDE 10

Typing

from typing import Dict, Union Order = Dict[str, Union[int,str]] def get(order_id: int) -> Order:

  • rder_name: str = 'EuroPython2019!'
  • rder: Order = {

'id': order_id, 'name': order_name } return order

slide-11
SLIDE 11

Typing

from typing import Dict, Union Order = Dict[str, Union[int,str]] def get(order_id: int) -> Order:

  • rder_name: str = 'EuroPython2019!'
  • rder: Order = {

'id': order_id, 'name': order_name } return order

slide-12
SLIDE 12

Typing

from typing import Dict, Union Order = Dict[str, Union[int,str]] def get(order_id: int) -> Order:

  • rder_name: str = 'EuroPython2019!'
  • rder: Order = {

'id': order_id, 'name': order_name } return order

slide-13
SLIDE 13

Python 3.7

slide-14
SLIDE 14

Dataclasses

from dataclasses import dataclass @dataclass class Order: id: int name: str

slide-15
SLIDE 15

Typing 💛 Dataclasses

from our_package.entities import Order def get(order_id: int) -> Order:

  • rder_name: str = EuroPython2019!'
  • rder: Order = Order(

id=order_id, name=order_name, ) return order

P R O T I P # 2

slide-16
SLIDE 16

Let's talk architecture

slide-17
SLIDE 17

How to design architecture?

With the idea of maintaining it for the next 10 years

slide-18
SLIDE 18

Clean architecture

slide-19
SLIDE 19

Time for “coding”

slide-20
SLIDE 20

Shop project - requirements

  • Creating orders
  • Viewing order lists
  • Add existing items to order
slide-21
SLIDE 21

Steps of building the application

  • data definition - entities
slide-22
SLIDE 22

Entities

class BaseEntity: pass @dataclass(frozen=True) class Client(BaseEntity): id: int name: str

slide-23
SLIDE 23

Entities

class BaseEntity: pass @dataclass(frozen=True) class Client(BaseEntity): id: int name: str

slide-24
SLIDE 24

Entities - frozen=True

>>> client = Client(id=1, name='Grzegorz') >>> client.name = 'Alice' Traceback (most recent call last): File "<input>", line 1, in <module> File "<string>", line 3, in __setattr__ dataclasses.FrozenInstanceError: cannot assign to field 'name'

slide-25
SLIDE 25

Entities

@dataclass(frozen=True) class Product(BaseEntity): id: int name: str price: float

slide-26
SLIDE 26

Entities

@dataclass(frozen=True) class Order(BaseEntity): id: int created: datetime client: Client total_cost: float = 0.0 items: ??

slide-27
SLIDE 27

Entities

@dataclass(frozen=True) class Order(BaseEntity): id: int created: datetime client: Client total_cost: float = 0.0 items: List[Product] = field(default_factory=list)

slide-28
SLIDE 28

Steps of building the application

  • data definition - entities
slide-29
SLIDE 29

Steps of building the application

  • data definition - entities
  • use cases
slide-30
SLIDE 30

Use cases

class OrderLogic: def create(self, client_id: int) -> Order: ... def search(self, client_id: int) -> List[Order]: ... def add_product(self, order_id: int, product_id: int) -> Order: ...

slide-31
SLIDE 31

But those are

  • perations on a

database …

slide-32
SLIDE 32

… it's time to introduce ...

slide-33
SLIDE 33

An abstraction

PRO TIP #3

slide-34
SLIDE 34

Clean architecture

slide-35
SLIDE 35

Repositories

class IClientRepository(abc.ABC): @abc.abstractmethod def create(self, name: str) -> Client: pass @abc.abstractmethod def get(self, client_id: int) -> Client: pass

slide-36
SLIDE 36

Repositories

class IClientRepository(abc.ABC): @abc.abstractmethod def create(self, name: str) -> Client: pass @abc.abstractmethod def get(self, client_id: int) -> Client: pass

slide-37
SLIDE 37

Repositories

class IClientRepository(abc.ABC): @abc.abstractmethod def create(self, name: str) -> Client: pass @abc.abstractmethod def get(self, client_id: int) -> Client: pass

slide-38
SLIDE 38

Repositories

class IClientRepository(abc.ABC): @abc.abstractmethod def create(self, name: str) -> Client: pass @abc.abstractmethod def get(self, client_id: int) -> Client: pass

slide-39
SLIDE 39

Repositories

class IClientRepository(abc.ABC): @abc.abstractmethod def create(self, name: str) -> Client: pass @abc.abstractmethod def get(self, client_id: int) -> Client: pass

slide-40
SLIDE 40

Repositories

class IClientRepository(abc.ABC): @abc.abstractmethod def create(self, name: str) -> Client: pass @abc.abstractmethod def get(self, client_id: int) -> Client: pass

slide-41
SLIDE 41

Repositories

class IProductRepository(abc.ABC): @abc.abstractmethod def get(self, product_id: int) -> Product: pass

slide-42
SLIDE 42

Repositories

class IOrderRepository(abc.ABC): def create(self, client: Client) -> Order: def get(self, order_id: int) -> Order: def save(self, order: Order) -> Order: def search( self, client: Optional[Client] = None ) -> List[Order]:

slide-43
SLIDE 43

Use cases

class OrderLogic: def create(self, client_id: int) -> Order: ... def search(self, client_id: int) -> List[Order]: ... def add_product(self, order_id: int, product_id: int) -> Order: ...

slide-44
SLIDE 44

Use cases

class OrderLogic: def __init__( self,

  • rders: IOrderRepository,

products: IProductRepository, clients: IClientRepository, ) -> None: self._orders: IOrderRepository = orders self._products: IProductRepository = products self._clients: IClientRepository = clients

slide-45
SLIDE 45

Use cases

class OrderLogic: @inject def __init__( self,

  • rders: IOrderRepository,

products: IProductRepository, clients: IClientRepository, ) -> None: self._orders: IOrderRepository = orders self._products: IProductRepository = products self._clients: IClientRepository = clients from injector import inject

P R O T I P # 4

slide-46
SLIDE 46

Use cases

class OrderLogic: def search(self, client_id: int) -> List[Order]: client = self._clients.get(client_id) return self._orders.search(client=client) def create(self, client_id: int) -> Order: client = self._clients.get(client_id) return self._orders.create(client=client)

slide-47
SLIDE 47

Use cases

class OrderLogic: def add_product(self, order_id: int, product_id: int) -> Order:

  • rder = self._orders.get(order_id)

product = self._products.get(product_id)

slide-48
SLIDE 48

Use cases

class OrderLogic: def add_product(self, order_id: int, product_id: int) -> Order:

  • rder = self._orders.get(order_id)

product = self._products.get(product_id)

  • rder = replace( # from dataclasses import replace
  • rder,

items=order.items + [product], total_cost=order.total_cost + product.price, )

slide-49
SLIDE 49

Use cases

class OrderLogic: def add_product(self, order_id: int, product_id: int) -> Order:

  • rder = self._orders.get(order_id)

product = self._products.get(product_id)

  • rder = replace( # from dataclasses import replace
  • rder,

items=order.items + [product], total_cost=order.total_cost + product.price, ) return self._orders.save(order)

slide-50
SLIDE 50

So much work…

why bother?

slide-51
SLIDE 51

Tests

def test_add_product_increases_order_total_cost( ) -> None:

P R O T I P # 5

slide-52
SLIDE 52

Tests

def test_add_product_increases_order_total_cost( prepare_repositories: StaticRepositories ) -> None: StaticRepositories = Tuple[ IOrderRepository, IProductRepository, IClientRepository ]

PRO TIP #6

slide-53
SLIDE 53

Tests

def test_add_product_increases_order_total_cost( prepare_repositories: StaticRepositories ) -> None:

slide-54
SLIDE 54

Tests

def test_add_product_increases_order_total_cost( prepare_repositories: StaticRepositories ) -> None: 1: logic = OrderLogic(*prepare_repositories) 2: order = logic.add_product(order_id=1, product_id=1) 3: assert order.total_cost == 100

slide-55
SLIDE 55

Steps of building the application

  • data definition - entities
  • use cases
slide-56
SLIDE 56

Steps of building the application

  • data definition - entities
  • use cases
  • implement interfaces
slide-57
SLIDE 57

Time for a database?

slide-58
SLIDE 58

No no no :)

slide-59
SLIDE 59

A repository

slide-60
SLIDE 60

A repository

slide-61
SLIDE 61

A repository in

slide-62
SLIDE 62

A repository in

slide-63
SLIDE 63

A repository in memory

slide-64
SLIDE 64

Repozytorium w pamięci

class RamStorage(Generic[T]): def __init__(self) -> None: self._storage: StorageType = {} T = TypeVar("T") StorageType = Dict[int, T] RamStorage[Client]() RamStorage[Order]()

slide-65
SLIDE 65

Repozytorium w pamięci

class RamStorage(Generic[T]): def __init__(self) -> None: self._storage: StorageType = {} def add(self, item: T) -> None: def get(self, pk: int) -> Optional[T]: def search(self, **kwargs: Any) -> RamStorage[T]: def remove(self, item: T) -> None: def all(self) -> List[T]:

slide-66
SLIDE 66

Repozytorium w pamięci - wykorzystanie

class ProductRepository(IProductRepository): def __init__(self) -> None: self._ram_storage = RamStorage[Product]()

slide-67
SLIDE 67

Repozytorium w pamięci - wykorzystanie

class ProductRepository(IProductRepository): def __init__(self) -> None: self._ram_storage = RamStorage[Product]() def get(self, product_id: int) -> Product: result = self._ram_storage.get(product_id) if result is None: raise ProductNotFound() return result

slide-68
SLIDE 68

Steps of building the application

  • data definition - entities
  • use cases
  • implement interfaces
slide-69
SLIDE 69

Steps of building the application

  • data definition - entities
  • use cases
  • implement interfaces
  • API
slide-70
SLIDE 70

Choose framework!

and this is slide no 77!

P R O T I P # 7

slide-71
SLIDE 71

Flask + Connexion

slide-72
SLIDE 72

Flask + connexion - api.yaml

paths: /orders/search/: get:

  • perationId: our_package.endpoints.search

parameters: ... responses: ...

slide-73
SLIDE 73

Flask + connexion - api.yaml

slide-74
SLIDE 74

Flask + connexion - endpoints

def search(logic: OrderLogic, client_id: int) -> List[Order]: return logic.search(client_id=client_id) def create(logic: OrderLogic, body: dict) -> Order: return logic.create(client_id=body['client_id']) def add_product(logic: OrderLogic, order_id: int, product_id: int) -> Order: return logic.add_product(order_id, product_id)

slide-75
SLIDE 75

Flask + connexion - endpoints

def search(logic: OrderLogic, client_id: int) -> List[Order]: return logic.search(client_id=client_id) def create(logic: OrderLogic, body: dict) -> Order: return logic.create(client_id=body['client_id']) def add_product(logic: OrderLogic, order_id: int, product_id: int) -> Order: return logic.add_product(order_id, product_id)

slide-76
SLIDE 76

Injector

slide-77
SLIDE 77

Flask + connexion - endpoints

flask_injector = FlaskInjector( app=app, modules=[MyModule] ) app.config["FLASK_INJECTOR"] = flask_injector

slide-78
SLIDE 78

Flask + connexion - endpoints

class MyModule(injector.Module): def configure(self, binder: injector.Binder) -> None:

slide-79
SLIDE 79

Flask + connexion - endpoints

class MyModule(injector.Module): def configure(self, binder: injector.Binder) -> None: binder.bind(OrderLogic, to=OrderLogic)

slide-80
SLIDE 80

Flask + connexion - endpoints

class MyModule(injector.Module): def configure(self, binder: injector.Binder) -> None: binder.bind(OrderLogic, to=OrderLogic) binder.bind( IClientRepository, to=ClientRepository, scope=injector.SingletonScope )

slide-81
SLIDE 81

Flask + connexion - endpoints

def search(logic: OrderLogic, client_id: int) -> List[Order]: return logic.search(client_id=client_id) def create(logic: OrderLogic, body: dict) -> Order: return logic.create(client_id=body['client_id']) def add_product(logic: OrderLogic, order_id: int, product_id: int) -> Order: return logic.add_product(order_id, product_id)

slide-82
SLIDE 82

Flask + connexion - endpoints

def search(logic: OrderLogic, client_id: int) -> List[Order]: return logic.search(client_id=client_id) def create(logic: OrderLogic, body: dict) -> Order: return logic.create(client_id=body['client_id']) def add_product(logic: OrderLogic, order_id: int, product_id: int) -> Order: return logic.add_product(order_id, product_id)

slide-83
SLIDE 83

Response serialization

slide-84
SLIDE 84

Flask + connexion - encoder class ApiJsonEncoder(JSONEncoder):

slide-85
SLIDE 85

Flask + connexion - encoder class ApiJsonEncoder(JSONEncoder): def default(self, obj: Any) -> Any: if isinstance(obj, (datetime.datetime, datetime.date)): return obj.isoformat()

slide-86
SLIDE 86

Flask + connexion - encoder class ApiJsonEncoder(JSONEncoder): def default(self, obj: Any) -> Any: if isinstance(obj, (datetime.datetime, datetime.date)): return obj.isoformat() if isinstance(obj, Client): return {"id": obj.id, "name": obj.name}

slide-87
SLIDE 87

Flask + connexion - encoder class ApiJsonEncoder(JSONEncoder): def default(self, obj: Any) -> Any: if isinstance(obj, (datetime.datetime, datetime.date)): return obj.isoformat() if isinstance(obj, BaseEntity): return {key: value for key, value in vars(obj).items() if value is not None}

slide-88
SLIDE 88

Flask + connexion - encoder class ApiJsonEncoder(JSONEncoder): def default(self, obj: Any) -> Any: if isinstance(obj, (datetime.datetime, datetime.date)): return obj.isoformat() if isinstance(obj, Base): return {key: value for key, value in vars(obj).items() if value is not None} return JSONEncoder.default(self, obj)

slide-89
SLIDE 89

Steps of building the application

  • data definition - entities
  • use cases
  • implement interfaces
  • API
slide-90
SLIDE 90

Clean architecture

slide-91
SLIDE 91

Protect borders

PRO TIP #8

slide-92
SLIDE 92

Borders - project structure

  • shop
  • ram_db
  • api
  • setup.py
slide-93
SLIDE 93

Borders - project structure

  • shop

○ setup.py ○ requirements.txt

  • ram_db

○ setup.py ○ requirements.txt

  • api

○ setup.py ○ requirements.txt

  • setup.py
slide-94
SLIDE 94

Granice - struktura projektu

  • shop

○ setup.py ○ requirements.txt -> injector

  • ram_db

○ setup.py ○ requirements.txt -> shop

  • api

○ setup.py ○ requirements.txt -> shop, ram_db, flask, connexion

  • setup.py
slide-95
SLIDE 95

Borders - project structure

  • shop

○ setup.py ○ requirements.txt

  • ram_db

○ setup.py ○ requirements.txt

  • api

○ setup.py ○ requirements.txt

  • setup.py
slide-96
SLIDE 96

DEMO

slide-97
SLIDE 97

Benefits

  • Business logic independence
  • Ease of technology update/change
  • Clear and secure borders
  • Technology chosen based on knowledge
  • Fast prototyping / POC
  • Ease of testing
  • Installing only required packages
slide-98
SLIDE 98

Architecture is a set of conscious decisions

slide-99
SLIDE 99

Clean architecture is a way to delay important decisions

slide-100
SLIDE 100

Further reading

Clean Architecture - Robert C. Martin https://g.co/kgs/kbaamc Python microlibs: https://medium.com/@jherreras/python-microlibs-5be9461ad979

slide-101
SLIDE 101

Thank you

grzegorz@kocjan.me @GrzegorzKocjan

https://migawka.it/europython2019