Clean Architecture
Python (web) apps
Przemek Lewandowski
@haxoza
Overview
- Entities
- Use cases
- Adapters
- Frameworks and Drivers
- Rules between layers
- Web vs Other UIs
- Example in Python
- Summary
Clean Architecture
- Independent of Frameworks
- Testable
- Independent of UI
- Independent of Database
- Independent of any external agency
Uncle's Bob Clean Architecture
Dependency Rule
- The further in you go, the higher level the software becomes.
- Source code dependencies can only point inward.
- Data formats used in an outer circle should not be used by an inner circle.
- Anything in an outer circle to impact the inner circles.
Entities
Enterprise wide business rules
Use cases
Application specific business rules
Interface Adapters
Data is converted from the form most convenient for entities and use cases to the format most convenient for some external agency such as the Database or the Web.
Frameworks and Drivers
- Tools such as the Database, the Web Framework, etc
- Glue code that communicates to the next circle inwards.
Crossing boundaries
- Dependency Rule
- Dependency Inversion Principle
- Data that crosses the boundaries is simple data structures.
Flow of control
Web vs Other UIs
- Web is state-less
- Clean Architecture principles
are more generic than Web - Presenters & Controllers can work
in different threads
Web vs Other UIs
Adapter
Use case
Example code
in Python
Entities
from typing import List
class Invoice:
def __init__(self, number: str, lines: List[InvoiceLine]):
self.number = number
self.lines = lines
self._total_net_value = None
def validate(self):
pass
@property
def total_net_value(self):
pass
@total_net_value.setter
def total_net_value(self, value):
pass
class InvoiceLine:
def __init__(self, description: str, net_value: int):
self.description = description
self.net_value = net_value
def validate(self):
pass
Use cases
from typing import Iterable, Dict, Optional, ContextManager
from core.domain.invoice import Invoice
class InvoiceUseCase:
def __init__(self, repository: InvoiceRepository):
self.repository = repository
def get_list(self, filters: Optional[Dict[str, str]]) -> Iterable[Invoice]:
return self.repository.get_list(filters)
def create(self, invoice: Invoice) -> Invoice:
for line in invoice.lines:
line.validate()
invoice.validate()
with self.repository.atomic():
self._validate_invoice_number(invoice)
invoice = self.repository.save(invoice)
return invoice
def _validate_invoice_number(self, invoice: Invoice):
pass
Repository interface
from typing import Iterable, Dict, Optional, ContextManager
from core.domain.invoice import Invoice
class InvoiceRepository:
def get_list(self, filters) -> Iterable[Invoice]:
raise NotImplementedError()
def save(self, invoice: Invoice) -> Invoice:
raise NotImplementedError()
def atomic(self) -> ContextManager:
raise NotImplementedError()
Adapters
from typing import Iterable, NamedTuple
class InvoiceLineData(NamedTuple):
description: str
net_value: int
class InvoiceData(NamedTuple):
number: str
lines: Iterable[InvoiceLineData]
Adapters
from typing import Iterable, Dict, Optional, NamedTuple
from core.domain.invoice import Invoice
from core.usecases.invoice import InvoiceUseCase
class InvoiceAdapter:
def __init__(self, repository: InvoiceRepository):
self.usecase = InvoiceUseCase(repository)
def get_list(self, filters: Optional[Dict[str, str]]) -> Iterable[InvoiceData]:
invoices = self.usecase.get_list(filters)
invoices_data = []
for invoice in invoices:
invoices_data.append(self._invoice_to_data(invoice))
return invoices_data
def create(self, invoice_data: InvoiceData) -> InvoiceData:
invoice = self._data_to_invoice(invoice_data)
invoice = self.usecase.create(invoice)
return self._invoice_to_data(invoice)
@classmethod
def _invoice_to_data(cls, invoice: Invoice) -> InvoiceData:
pass
@classmethod
def _data_to_invoice(cls, invoice_data: InvoiceData) -> Invoice:
pass
Flask View
import json
from flask import request
from flask.ext.restful import Resource, Api
from core.adapters import InvoiceAdapter
from database import InvoiceRepository
class InvoiceResource(Resource):
default_length = 100
def __init__(self, *args, **kwargs):
self.super().__init__(*args, **kwargs)
self.adapter = InvoiceAdapter(InvoiceRepository())
def get(self, number=None):
if number:
return self.get_one(number)
return self.get_list()
def get_one(self, number):
invoice = self.adapter.get_list({'number': number})
return json.dumps(invoice)
def get_list(self):
invoices = self.adapter.get_list()
return json.dumps(invoices)
api = Api()
api.add_resource(InvoiceResource, '/invoices/<int:number>', '/invoices')
Summary
- Everyone needs to respect rules
- Significant boilerplate
- Difficult to leverage frameworks
- Beloved Active Record pattern does not help
Resources
- https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
- http://blog.thedigitalcatonline.com/blog/2016/11/14/clean-architectures-in-python-a-step-by-step-example/
- https://docs.python.org/3/library/typing.html
Git repo
TBA soon
Join the team!
Thanks!
Questions?
@haxoza
Clean architecture - Python (web) apps
By Przemek Lewandowski
Clean architecture - Python (web) apps
- 9,522