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,241