You are on page 1of 151

Software Architecture

Copyright © 2021, 2022 Lan Hui


Course information (Spring 2022)

This full-semester course introduces important architectural


patterns for making software easier to build, test, understand
and maintain.
Architecture: carefully designed structure of something.
Required textbooks: None.
Recommended textbooks:

• Harry Percival and Bob Gregory. Architecture Patterns with


Copyright © 2021, 2022 Lan Hui 1
Python. O’Reilly Media; 1st edition (March 31, 2020).

• Dusty Phillips. Python 3 Object Oriented Programming.


Packt Publishing; 3rd edition (October 30, 2018).

A significant portion of the lecture notes is built on the


above two books.

Recommended reference books:

• Eric Evans. Domain-Driven Design: Tackling Complexity in


the Heart of Software. Addison Wesley, 2003.
Copyright © 2021, 2022 Lan Hui 2
• Martin Fowler. Patterns of Enterprise Application
Architecture. Addison Wesley, 2002.

Grading policy:
Component Weight
Labs 40
Course project and discussion 20
Final exam 40
Software freely available for this course:

• Python 3.9.7 interpreter:


https://www.python.org/downloads/
Copyright © 2021, 2022 Lan Hui 3
• Wing IDE 101: http://wingware.com/downloads/wing-101

• Flask: https://pypi.org/project/Flask

• SQLAlchemy: https://pypi.org/project/SQLAlchemy

• ponyORM: https://ponyorm.org/ (easier to use than


SQLAlchemy)

Copyright © 2021, 2022 Lan Hui 4


What Is Wrong?

Big ball of mud.

Big ball of yarn.

Figure 1. A real-life dependency diagram

Both refer to a software application that lacks a perceivable


architecture.

Caution: with a circular layout, how messy the graph looks


depends on how the nodes are arranged. So you cannot simply
Copyright © 2021, 2022 Lan Hui 5
say the software lacks architecture if it looks like a big ball of
mud with the circle layout.

Online demo tool: https://dreampuf.github.io/GraphvizOnline


digraph {
1=>2;
2=>3;
1=>3;
3=>4;
}

Copyright © 2021, 2022 Lan Hui 6


Treating Software as Garden

Analogy: The garden runs wild without maintenance.


There is a persistent notion in a lot of literature that
software development should be like engineering. First, an
architect draws up some great plans. Then you get a flood of
warm bodies to come in and fill the chairs, bang out all the
code, and you’re done. A lot of people still feel that way. I saw
an interview in the last six months of a big outsourcing house
in India where this was how they felt. They paint a picture of
constructing software like buildings. The high talent architects
Copyright © 2021, 2022 Lan Hui 7
do the design. The coders do the constructing. The tenants
move in, and everyone lives happily ever after. We don’t think
that’s very realistic. It doesn’t work that way with software. -
Andy Hunt
From Software Development as Gardening.
Chaotic software application – everything is coupled to
everything else and changing any part is likely to break other
parts.
A common problem: business logic spread over all places.
A common practice: build a database schema first. Object
model? Rarely thought about that.
Copyright © 2021, 2022 Lan Hui 8
Examples:

ponyORM eStore

ponyORM PhotoSharing

ponyORM Biopsy

Copyright © 2021, 2022 Lan Hui 9


Why Architecture?

MANAGE COMPLEXITY.
FOR GROWTH.
Separate business logic from infrastructure concerns.
Structure the application to facilitate Pyramid Testing (unit,
integration, end-to-end).
Need efforts.
Layered design/archictecture (presentation layer – business
Copyright © 2021, 2022 Lan Hui 10
logic – database layer). See Figure 5-2 Typical Application
Layers. Do not cross layers. Like our government structure?

Layers, layers, layers. Folders, folders, folders. See Appendix


B: Project Structure in the textbook.

Levels of abstractions. (Note: this abstraction is different


the abstraction we talked in the OOAD course. In this course,
we call an object or function an abstraction.)

Different names: Hexagonal Architecture, Ports-and-


Adapters, Onion Architecture, Clean Architecture.

Figure 5-7 Onion view of clean architecture.


Copyright © 2021, 2022 Lan Hui 11
Figure 3. Onion architecture in Chapter 2 of the textbook.

The OAPS Class Diagram from LiuSiyuan et al.

Copyright © 2021, 2022 Lan Hui 12


Undesirable Things during Software
Development

• Complexity.

• Big project size.

• Changing requirements.

• Regression.

• High coupling.
Copyright © 2021, 2022 Lan Hui 13
• High cost in maintaining tests.

• Slow speed while testing the UI layer or the real data layer.

• Requiring a test database.

Copyright © 2021, 2022 Lan Hui 14


Encapsulation and Abstractions

Encapsulating behavior using abstractions.


Think on the level of behavior, not on the level of data
or algorithm. BDD: behavior should come first and drive our
storage requirements..
How to understand abstraction? Public API. Interface.
Function. Method.
Responsibility: search sausages from duckduckgo.com.
A low-level abstraction
Copyright © 2021, 2022 Lan Hui 15
import j s o n
from u r l l i b . r e q u e s t import u r l o p e n
from u r l l i b . p a r s e import u r l e n c o d e

params = d i c t ( q= ’ S a u s a g e s ’ , format= ’ j s o n ’ )
handle = u r l o p e n ( ’ h t t p : / / a p i . duckduckgo . com ’ + ’ ? ’ + u r l e n c o d e ( params )
raw text = handle . read ( ) . decode ( ’ u t f 8 ’ )
parsed = json . loads ( raw text )

r e s u l t s = parsed [ ’ RelatedTopics ’ ]
for r in r e s u l t s :
i f ’ Text ’ i n r :
p r i n t ( r [ ’ F i r s t U R L ’ ] + ’ = ’ + r [ ’ Text ’ ] )

A medium-level abstraction
import r e q u e s t s

Copyright © 2021, 2022 Lan Hui 16


params = d i c t ( q= ’ S a u s a g e s ’ , format= ’ j s o n ’ )
p a r s e d = r e q u e s t s . g e t ( ’ h t t p : / / a p i . duckduckgo . com/ ’ , params=params ) . j s o

r e s u l t s = parsed [ ’ RelatedTopics ’ ]
for r in r e s u l t s :
i f ’ Text ’ i n r :
p r i n t ( r [ ’ F i r s t U R L ’ ] + ’ = ’ + r [ ’ Text ’ ] )

A high-level abstraction
import duckduckpy
f o r r i n duckduckpy . q u e r y ( ’ S a u s a g e s ’ ) . r e l a t e d t o p i c s :
print ( r . f i r s t u r l , ’ = ’ , r . text )

Which one is easier to understand?

Exercise Check the source code of the package duckduckpy.


Copyright © 2021, 2022 Lan Hui 17
Making a package is a way to providing abstraction. Learn how
to make a Python package. Do you have any project idea?
Can you make a package and publish it at PyPI?

Copyright © 2021, 2022 Lan Hui 18


Packaging

Packaging is a basic technique for the architecture.

A package includes several related modules, each of which


includes several related classes or functions.

In Python, a package is organized in a folder, while a module


is organized in a py file.

Check the directory structure in Chapter 4 of the textbook.

Render the following dot graph using Graphviz Online:


Copyright © 2021, 2022 Lan Hui 19
digraph G {

subgraph c l u s t e r d o m a i n {
subgraph c l u s t e r 1 {
a l l o c a t e [ c o l o r=g r a y , s t y l e= f i l l e d ] ;
OutOfStock ;
OrderLine ;
Batch
l a b e l = ” model . py ” ;
}
l a b e l = ”Domain ” ;
}

subgraph c l u s t e r a d a p t e r {
subgraph c l u s t e r 2 {
AbstractRepository ;
SqlAlchemyRepository ;

Copyright © 2021, 2022 Lan Hui 20


l a b e l = ” r e p o s i t o r y . py ” ;
}
subgraph c l u s t e r 3 {
s t a r t m a p p e r s [ c o l o r=g r a y , s t y l e= f i l l e d ] ;
l a b e l = ”orm . py ” ;
}
l a b e l = ” Adapters ” ;
}

How many packages do we see?

How many modules are there in each package?

What are the function? What are the classes?

Copyright © 2021, 2022 Lan Hui 21


Layering

When do we say one depends on the other?

Depends: uses, needs, knows, calls, imports.

Dependency graph/network.

Figure 2. Layered architecture

Things in Presentation Layer should not use things in


Database Layer.

Copyright © 2021, 2022 Lan Hui 22


Domain Modeling

Domain ≈ Business Logic.


Domain Model ≈ Business Logic Layer
Pay attention to business jargons.
Seperate business logic from infrastructure and from user-
interface logic.
Domain model: a mental map that business owners have
of their business, expressed in domain-specific terminologies.
Describe the business core (most valuable and likely to change).
Copyright © 2021, 2022 Lan Hui 23
Why? Keep the model decoupled from infrastructural
concerns.
Figure 1. A component diagram for our app at the end of
Building an Architecture to Support Domain Modeling
Important concepts:

• Value Object: uniquely identified by the data it holds (not


by a long-lived ID).
@ d a t a c l a s s ( f r o z e n=True )
class OrderLine :
orderid : OrderReference
sku : ProductReference
qty : Quantity

Copyright © 2021, 2022 Lan Hui 24


• Entity: uniquely identified by a unique ID.

• Aggregate.

• Domain Service.

What is domain? The problem (we are going to solve).


Example: an online furniture retailer (purchasing, product
design, logistics, delivery, etc).
Business jargons (domain language): source, goods, SKU
(stock-keeping unit), supplier, stock, out of stock, warehouse,
inventory, lead time, ETA, batch, order, order reference,
Copyright © 2021, 2022 Lan Hui 25
order line, quantity, allocate, deliver, ship, shipment tracking,
dispatch, etc. Check MADE.com.

What is model? A map of a process that captures useful


property.

Data model: database, database schema.

Decouple it (the model) from technical concerns (or


infrastructure concerns).

Related topic: DDD.

Tips:
Copyright © 2021, 2022 Lan Hui 26
1. Ask the domain expert for glossary and concrete examples.

2. Start with a minimal version of the domain model.

3. Make a minimum viable product (MVP) as quickly as


possible.

4. Add new classes instead of changing existing ones. Reason:


won’t affect existing code.

5. Do not share a database between two applications.

** Talk over Some Notes on Allocation.


Copyright © 2021, 2022 Lan Hui 27
Major task: allocate order lines to batches.

Our domain model expressed in Python:


from d a t a c l a s s e s import d a t a c l a s s
from t y p i n g import O p t i o n a l
from d a t e t i m e import d a t e

@ d a t a c l a s s ( f r o z e n=True ) #( 1 ) ( 2 )
class OrderLine :
orderid : str
sku : s t r
qty : int

c l a s s Batch :
def init ( s e l f , r e f : str , sku : str , qty : int , eta : O p t i o n a l [ dat
#( 2 )

Copyright © 2021, 2022 Lan Hui 28


self . reference = ref
self . sku = sku
self . eta = eta
self . a v a i l a b l e q u a n t i t y = qty

def a l l o c a t e ( s e l f , l i n e : O r d e r L i n e ) :
s e l f . a v a i l a b l e q u a n t i t y == l i n e . q t y #( 3 )

def c a n a l l o c a t e ( s e l f , l i n e : O r d e r L i n e ) => bool :


r e t u r n s e l f . s k u == l i n e . s k u and s e l f . a v a i l a b l e q u a n t i t y >= l i n

Our model in a graphic form:


Copyright © 2021, 2022 Lan Hui 29
Real-world business has many edge cases:

• Deliver on a specific date.

• Deliver from a warehourse in a different region than the


customer’s location.

The EnglishPal story: the web UI was built after the domain
Copyright © 2021, 2022 Lan Hui 30
model had been built. In fact, I never thought I would build a
web application for EnglishPal.

Copyright © 2021, 2022 Lan Hui 31


MADE.com

Figure 2. Context diagram for the allocation service

- A global supply chain.

- Freight partners.

- Manufacturers.

- Minimum storage. Treat goods on the road as stock.

Copyright © 2021, 2022 Lan Hui 32


Unit Testing Domain Models

Use unit test to describe the behavior of the system.

Think about how to use it before implementing it.

The test describes the behavior of the system from an


interface point of view.
def t e s t a l l o c a t i n g t o a b a t c h r e d u c e s t h e a v a i l a b l e q u a n t i t y ( ) :

b a t c h = Batch ( ” batch = 001” , ”SMALL=TABLE” , q t y =20 , e t a=d a t e . t o d a y ( )


l i n e = O r d e r L i n e ( ” o r d e r = r e f ” , ”SMALL=TABLE” , 2 )

batch . a l l o c a t e ( l i n e )

Copyright © 2021, 2022 Lan Hui 33


a s s e r t b a t c h . a v a i l a b l e q u a n t i t y == 18

### More t e s t s ###


def m a k e b a t c h a n d l i n e ( sku , b a t c h q t y , l i n e q t y ) :
return (
Batch ( ” batch = 001” , sku , b a t c h q t y , e t a=d a t e . t o d a y ( ) ) ,
O r d e r L i n e ( ” o r d e r = 123” , sku , l i n e q t y ) ,
)

def t e s t c a n a l l o c a t e i f a v a i l a b l e g r e a t e r t h a n r e q u i r e d ( ) :
l a r g e b a t c h , s m a l l l i n e = m a k e b a t c h a n d l i n e ( ”ELEGANT=LAMP” , 2 0 ,
assert large batch . can allocate ( small line )

def t e s t c a n n o t a l l o c a t e i f a v a i l a b l e s m a l l e r t h a n r e q u i r e d ( ) :
s m a l l b a t c h , l a r g e l i n e = m a k e b a t c h a n d l i n e ( ”ELEGANT=LAMP” , 2 , 2
assert small batch . can allocate ( l a r g e l i n e ) is False

Copyright © 2021, 2022 Lan Hui 34


def t e s t c a n a l l o c a t e i f a v a i l a b l e e q u a l t o r e q u i r e d ( ) :
batch , l i n e = m a k e b a t c h a n d l i n e ( ”ELEGANT=LAMP” , 2 , 2 )
a s s e r t batch . c a n a l l o c a t e ( l i n e )

def t e s t c a n n o t a l l o c a t e i f s k u s d o n o t m a t c h ( ) :
b a t c h = Batch ( ” batch = 001” , ”UNCOMFORTABLE=CHAIR” , 1 0 0 , e t a=None )
d i f f e r e n t s k u l i n e = O r d e r L i n e ( ” o r d e r = 123” , ”EXPENSIVE=TOASTER” , 1
a s s e r t batch . c a n a l l o c a t e ( d i f f e r e n t s k u l i n e ) i s F a l s e

• allocate

• can allocate

• deallocate
Copyright © 2021, 2022 Lan Hui 35
def t e s t c a n o n l y d e a l l o c a t e a l l o c a t e d l i n e s ( ) :
batch , u n a l l o c a t e d l i n e = m a k e b a t c h a n d l i n e ( ”DECORATIVE=TRINKET”
batch . d e a l l o c a t e ( u n a l l o c a t e d l i n e )
a s s e r t b a t c h . a v a i l a b l e q u a n t i t y == 20

Updated model:
c l a s s Batch :
def i n i t ( s e l f , r e f : str , sku : str , qty : int , eta : O p t i o n a l [ dat
self . reference = ref
s e l f . sku = sku
s e l f . eta = eta
s e l f . p u r c h a s e d q u a n t i t y = qty
s e l f . a l l o c a t i o n s = s e t ( ) # t y p e : S e t [ OrderLine ]

def a l l o c a t e ( s e l f , l i n e : O r d e r L i n e ) :
if self . can allocate ( line ):
s e l f . a l l o c a t i o n s . add ( l i n e )

Copyright © 2021, 2022 Lan Hui 36


def d e a l l o c a t e ( s e l f , l i n e : O r d e r L i n e ) :
i f l i n e in s e l f . a l l o c a t i o n s :
s e l f . a l l o c a t i o n s . remove ( l i n e )

@property
def a l l o c a t e d q u a n t i t y ( s e l f ) => i n t :
r e t u r n sum( l i n e . q t y f o r l i n e i n s e l f . a l l o c a t i o n s )

@property
def a v a i l a b l e q u a n t i t y ( s e l f ) => i n t :
return s e l f . p u r c h a s e d q u a n t i t y = s e l f . a l l o c a t e d q u a n t i t y

def c a n a l l o c a t e ( s e l f , l i n e : O r d e r L i n e ) => bool :


r e t u r n s e l f . s k u == l i n e . s k u and s e l f . a v a i l a b l e q u a n t i t y >= l i n

Can the above model pass the following test?

Copyright © 2021, 2022 Lan Hui 37


def t e s t a l l o c a t i o n i s i d e m p o t e n t ( ) :
batch , l i n e = m a k e b a t c h a n d l i n e ( ”ANGULAR=DESK” , 2 0 , 2 )
batch . a l l o c a t e ( l i n e )
batch . a l l o c a t e ( l i n e )
a s s e r t b a t c h . a v a i l a b l e q u a n t i t y == 18

Easy to test as no database is involved.

Copyright © 2021, 2022 Lan Hui 38


Value Objects

For something that has data but no ID. For example, order
line, name, and money.

Value Object pattern. Value objects have value equality.

Orer line:
@ d a t a c l a s s ( f r o z e n=True )
class OrderLine :
orderid : OrderReference
sku : ProductReference
qty : Quantity

Copyright © 2021, 2022 Lan Hui 39


Name and money:
from d a t a c l a s s e s import d a t a c l a s s
from t y p i n g import NamedTuple
from c o l l e c t i o n s import n a m e d t u p l e

@ d a t a c l a s s ( f r o z e n=True )
c l a s s Name :
first name : str
surname : s t r

c l a s s Money ( NamedTuple ) :
currency : str
value : int

Line = namedtuple ( ’ Line ’ , [ ’ sku ’ , ’ qty ’ ] )

def t e s t e q u a l i t y ( ) :
a s s e r t Money ( ’ gbp ’ , 1 0 ) == Money ( ’ gbp ’ , 1 0 )

Copyright © 2021, 2022 Lan Hui 40


a s s e r t Name ( ’ H a r r y ’ , ’ P e r c i v a l ’ ) != Name ( ’ Bob ’ , ’ G r e g o r y ’ )
a s s e r t L i n e ( ’RED=CHAIR ’ , 5 ) == L i n e ( ’RED=CHAIR ’ , 5 )

def t e s t n a m e e q u a l i t y ( ) :
a s s e r t Name ( ” H a r r y ” , ” P e r c i v a l ” ) != Name ( ” B a r r y ” , ” P e r c i v a l ” )

Math for value objects:


f i v e r = Money ( ’ gbp ’ , 5 )
t e n n e r = Money ( ’ gbp ’ , 1 0 )

def c a n a d d m o n e y v a l u e s f o r t h e s a m e c u r r e n c y ( ) :
a s s e r t f i v e r + f i v e r == t e n n e r

def c a n s u b t r a c t m o n e y v a l u e s ( ) :
a s s e r t t e n n e r = f i v e r == f i v e r

def a d d i n g d i f f e r e n t c u r r e n c i e s f a i l s ( ) :

Copyright © 2021, 2022 Lan Hui 41


with pytest . r a i s e s ( ValueError ) :
Money ( ’ usd ’ , 1 0 ) + Money ( ’ gbp ’ , 1 0 )

def c a n m u l t i p l y m o n e y b y a n u m b e r ( ) :
a s s e r t f i v e r * 5 == Money ( ’ gbp ’ , 2 5 )

def m u l t i p l y i n g t w o m o n e y v a l u e s i s a n e r r o r ( ) :
with p y t e s t . r a i s e s ( TypeError ) :
tenner * f i v e r

Copyright © 2021, 2022 Lan Hui 42


Entities

For something with a permanent ID (that is identified by


a reference). For example, a person (who has a permanent
identity).
c l a s s Person :

def i n i t ( s e l f , name : Name ) :


s e l f . name = name

def t e s t b a r r y i s h a r r y ( ) :
h a r r y = P e r s o n ( Name ( ” H a r r y ” , ” P e r c i v a l ” ) )
barry = harry

Copyright © 2021, 2022 Lan Hui 43


b a r r y . name = Name ( ” B a r r y ” , ” P e r c i v a l ” )

a s s e r t h a r r y i s b a r r y and b a r r y i s h a r r y

Entities have identity equality. Unlike value objects, an


entitie’s attributes can change without making it a different
entity (as long as its identity keeps the same).

Define equality operator ( eq ) on entities:


c l a s s Batch :
...

def eq ( self , other ):


i f not i s i n s t a n c e ( o t h e r , Batch ) :
return False

Copyright © 2021, 2022 Lan Hui 44


r e t u r n o t h e r . r e f e r e n c e == s e l f . r e f e r e n c e

def hash ( self ):


r e t u r n hash ( s e l f . r e f e r e n c e )

Copyright © 2021, 2022 Lan Hui 45


Domain Service

It is just a function.

allocate(): A service that allocates an order line, given a


list of batches.

Testing script:
def t e s t p r e f e r s c u r r e n t s t o c k b a t c h e s t o s h i p m e n t s ( ) :
i n s t o c k b a t c h = Batch ( ” i n = s t o c k = b a t c h ” , ”RETRO=CLOCK” , 1 0 0 , e t a=N
s h i p m e n t b a t c h = Batch ( ” s h i p m e n t = b a t c h ” , ”RETRO=CLOCK” , 1 0 0 , e t a=t
l i n e = O r d e r L i n e ( ” o r e f ” , ”RETRO=CLOCK” , 1 0 )

a l l o c a t e ( line , [ in stock batch , shipment batch ] )

Copyright © 2021, 2022 Lan Hui 46


a s s e r t i n s t o c k b a t c h . a v a i l a b l e q u a n t i t y == 90
a s s e r t s h i p m e n t b a t c h . a v a i l a b l e q u a n t i t y == 100

def t e s t p r e f e r s e a r l i e r b a t c h e s ( ) :
e a r l i e s t = Batch ( ” s p e e d y = b a t c h ” , ”MINIMALIST=SPOON” , 1 0 0 , e t a=t o d a
medium = Batch ( ” normal = b a t c h ” , ”MINIMALIST=SPOON” , 1 0 0 , e t a=tomorr
l a t e s t = Batch ( ” slow = b a t c h ” , ”MINIMALIST=SPOON” , 1 0 0 , e t a= l a t e r )
l i n e = O r d e r L i n e ( ” o r d e r 1 ” , ”MINIMALIST=SPOON” , 1 0 )

a l l o c a t e ( l i n e , [ medium , e a r l i e s t , l a t e s t ] )

a s s e r t e a r l i e s t . a v a i l a b l e q u a n t i t y == 90
a s s e r t medium . a v a i l a b l e q u a n t i t y == 100
a s s e r t l a t e s t . a v a i l a b l e q u a n t i t y == 100

Copyright © 2021, 2022 Lan Hui 47


def t e s t r e t u r n s a l l o c a t e d b a t c h r e f ( ) :
i n s t o c k b a t c h = Batch ( ” i n = s t o c k = batch = r e f ” , ”HIGHBROW=POSTER” , 1 0
s h i p m e n t b a t c h = Batch ( ” s h i p m e n t = batch = r e f ” , ”HIGHBROW=POSTER” , 1 0
l i n e = O r d e r L i n e ( ” o r e f ” , ”HIGHBROW=POSTER” , 1 0 )
a l l o c a t i o n = a l l o c a t e ( line , [ in stock batch , shipment batch ] )
a s s e r t a l l o c a t i o n == i n s t o c k b a t c h . r e f e r e n c e

Model:
def a l l o c a t e ( l i n e : O r d e r L i n e , b a t c h e s : L i s t [ Batch ] ) => s t r :
b a t c h = next ( b f o r b i n s o r t e d ( b a t c h e s ) i f b . c a n a l l o c a t e ( l i n e ) )
batch . a l l o c a t e ( l i n e )
return batch . r e f e r e n c e

For the sorted() method to work, we need to define an


order function gt :
c l a s s Batch :

Copyright © 2021, 2022 Lan Hui 48


...

def g t ( self , other ):


i f s e l f . e t a i s None :
return False
i f o t h e r . e t a i s None :
r e t u r n True
return s e l f . eta > other . eta

Tips:

• Instead of FooManager, use manage foo().

• Instead of BarBuilder, use build bar().

• Instead of BazFactory, use get baz().


Copyright © 2021, 2022 Lan Hui 49
Use Exceptions to Express Domain Concepts

A domain concept: out of stock.


def t e s t r a i s e s o u t o f s t o c k e x c e p t i o n i f c a n n o t a l l o c a t e ( ) :
b a t c h = Batch ( ” b a t c h 1 ” , ”SMALL=FORK” , 1 0 , e t a=t o d a y )
a l l o c a t e ( O r d e r L i n e ( ” o r d e r 1 ” , ”SMALL=FORK” , 1 0 ) , [ b a t c h ] )

w i t h p y t e s t . r a i s e s ( OutOfStock , match=”SMALL=FORK” ) :
a l l o c a t e ( O r d e r L i n e ( ” o r d e r 2 ” , ”SMALL=FORK” , 1 ) , [ b a t c h ] )

Define the exception class:


c l a s s OutOfStock ( E x c e p t i o n ) :
pass

Copyright © 2021, 2022 Lan Hui 50


def a l l o c a t e ( l i n e : O r d e r L i n e , b a t c h e s : L i s t [ Batch ] ) => s t r :
try :
b a t c h = next (
...
except S t o p I t e r a t i o n :
r a i s e OutOfStock ( f ”Out o f s t o c k f o r s k u { l i n e . s k u } ” )

Copyright © 2021, 2022 Lan Hui 51


The Domain Model in A Graphical Form

Figure 4. Our domain model at the end of the chapter

What do we lack? A database!

Copyright © 2021, 2022 Lan Hui 52


Business Logic

Ball-of-mud Problem: business logic spreads all over the


place. ”Business logic” classes that perform no calculations
but do perform I/O.

Customers do not care about how the data is stored (MySQL,


SQLite, Redis, etc).

What do they care?

Domain model includes business logic.

Copyright © 2021, 2022 Lan Hui 53


Hiding Implementation Details

Wrong: build a database schema first for whatever software


application.

Correct: behavior should come first and drive our storage


requirements.

Public contract.

Copyright © 2021, 2022 Lan Hui 54


Architectural Patterns

• Repository pattern - for persistent storage.

• Service layer - for use cases.

• Unit of Work pattern - for atomic operations.

Copyright © 2021, 2022 Lan Hui 55


Repository Patterns

Infrastructure ≈ Database

The infrastructure usually contains many technical details.


These technical details vary from one infrastructure to another.

We don’t want our domain model to depend on infrastructure


in afraid of slowing down unit tests or making changes hard.

Use a repository to abstract away details.

Avoid using raw SQL statements in the domain model.


Copyright © 2021, 2022 Lan Hui 56
An abstraction over persistent storage — repo.add(something),
repo.get(id).

A repository pattern pretends that all data is in memory.

Usage example:
import a l l m y d a t a

def c r e a t e a b a t c h ( ) :
b a t c h = Batch ( . . . )
a l l m y d a t a . b a t c h e s . add ( b a t c h )

def m o d i f y a b a t c h ( b a t c h i d , n e w q u a n t i t y ) :
batch = a l l m y d a t a . batches . get ( b a t c h i d )
batch . c h a n g e i n i t i a l q u a n t i t y ( new quantity )

Copyright © 2021, 2022 Lan Hui 57


Abstract Repository (allowing us to use repo.get() instead
of session.query()):
c l a s s A b s t r a c t R e p o s i t o r y ( abc . ABC ) :
@abc . a b s t r a c t m e t h o d #( 1 )
def add ( s e l f , b a t c h : model . Batch ) :
r a i s e N o t I m p l e m e n t e d E r r o r #( 2 )

@abc . a b s t r a c t m e t h o d
def g e t ( s e l f , r e f e r e n c e ) => model . Batch :
ra is e NotImplementedError

• Save batch info to database.

• Retrieve batch info from database.


Copyright © 2021, 2022 Lan Hui 58
A repository object sits between our domain model and the
database. Check Figure 5. Repository pattern in Chapter 2.

Check Figure 1. Before and after the Repository pattern in


Chapter 2.

Check Figure 3 (Onion Architecture) in Chapter 2.

Dependency Inversion Principle (DIP): high-level


modules (the domain) should not depend on low-level ones
(the infrastructure).

Related topic: ORM.

Copyright © 2021, 2022 Lan Hui 59


DIP

Why? Decouple core logic from infrastructural concerns.

What?

• High-level modules should not depend on low-level modules.


Both should depend on abstractions.

• Abstractions should not depend on details. Instead, details


should depend on abstractions.
Copyright © 2021, 2022 Lan Hui 60
Low-level things: file systems, sockets, SMTP, IMAP, HTTP,
cron job, Kubernetes, etc.

How? Depending on abstractions. Interfaces. Let


infrastructure depend on domain.
def a l l o c a t e ( l i n e : O r d e r L i n e , r e p o : A b s t r a c t R e p o s i t o r y , s e s s i o n ) => st

We can use SqlAlchemyRepository or CsvRepository.

Check Figure 4-1. Direct denpendency graph and Figure


4-2. Inverted dependency graph for illustration.

Benfits: allowing high-level modules to change more


independently of low-level modules.

Copyright © 2021, 2022 Lan Hui 61


Persisting the Domain Model

Need a database.

1. Retrieve batch information from database.

2. Instantiate domain objects.


@ f l a s k . r o u t e . g u b b i n s # g u b b i n s means s t u f f
def a l l o c a t e e n d p o i n t ( ) :
# e x t r a c t o r d e r l i n e from r e q u e s t
l i n e = O r d e r L i n e ( r e q u e s t . params , . . . )
# l o a d a l l b a t c h e s from t h e DB
batches = . . .

Copyright © 2021, 2022 Lan Hui 62


# c a l l our domain s e r v i c e
a l l o c a t e ( line , batches )
# t h e n s a v e t h e a l l o c a t i o n b a c k t o t h e d a t a b a s e somehow
r e t u r n 201

Translate the model to a relational database.

Copyright © 2021, 2022 Lan Hui 63


Model Depends on ORM

ORM - object-relational mapper

O - the world of objects and domain modeling.

R - the world of databases and relational algebra.

Benefit of ORM: persistence ignorance.

Heavily depend on SQLAlchemy’s ORM now:


from s q l a l c h e m y import Column , F o r e i g n K e y , I n t e g e r , S t r i n g
from s q l a l c h e m y . e x t . d e c l a r a t i v e import d e c l a r a t i v e b a s e
from s q l a l c h e m y . orm import r e l a t i o n s h i p

Copyright © 2021, 2022 Lan Hui 64


Base = d e c l a r a t i v e b a s e ( )

c l a s s O r d e r ( Base ) :
i d = Column ( I n t e g e r , p r i m a r y k e y=True )

c l a s s O r d e r L i n e ( Base ) :
i d = Column ( I n t e g e r , p r i m a r y k e y=True )
s k u = Column ( S t r i n g ( 2 5 0 ) )
qty = I n t e g e r ( S t r i n g (250))
o r d e r i d = Column ( I n t e g e r , F o r e i g n K e y ( ’ o r d e r . i d ’ ) )
o r d e r = r e l a t i o n s h i p ( Order )

c l a s s A l l o c a t i o n ( Base ) :
...

Is the above model indenpendent (ignorant) of database?


Copyright © 2021, 2022 Lan Hui 65
No. It is coupled to Column!

Who depends on whom? Look at who uses (imports) whom.

Copyright © 2021, 2022 Lan Hui 66


ORM Depends on Model

Principle: Persistence Ignorance (PI). Ignore particular


database technologies.
Purpose: make the domain model stay ”pure”.
Explicit ORM mapping with SQLAlchemy Table objects.
Note the statement import model.
# orm . py
from s q l a l c h e m y . orm import mapper , r e l a t i o n s h i p

import model #( 1 )

Copyright © 2021, 2022 Lan Hui 67


metadata = MetaData ( )

o r d e r l i n e s = T a b l e ( #( 2 )
”order lines” ,
metadata ,
Column ( ” i d ” , I n t e g e r , p r i m a r y k e y=True , a u t o i n c r e m e n t=True ) ,
Column ( ” s k u ” , S t r i n g ( 2 5 5 ) ) ,
Column ( ” q t y ” , I n t e g e r , n u l l a b l e=F a l s e ) ,
Column ( ” o r d e r i d ” , S t r i n g ( 2 5 5 ) ) ,
)

...

def s t a r t m a p p e r s ( ) :
l i n e s m a p p e r = mapper ( model . O r d e r L i n e , o r d e r l i n e s ) #( 3 )

Copyright © 2021, 2022 Lan Hui 68


Result: model (model.py) stays “pure”. Let ORM (orm.py)
do ugly things.

Copyright © 2021, 2022 Lan Hui 69


Testing The ORM

session: a SQLAlchemy database session.


def t e s t o r d e r l i n e m a p p e r c a n l o a d l i n e s ( s e s s i o n ) : #( 1 )
session . execute (
”INSERT INTO o r d e r l i n e s ( o r d e r i d , sku , q t y ) VALUES ”
’ ( ” o r d e r 1 ” , ”RED=CHAIR ” , 1 2 ) , ’
’ ( ” o r d e r 1 ” , ”RED=TABLE” , 1 3 ) , ’
’ ( ” o r d e r 2 ” , ”BLUE=LIPSTICK ” , 1 4 ) ’
)
expected = [
model . O r d e r L i n e ( ” o r d e r 1 ” , ”RED=CHAIR” , 1 2 ) ,
model . O r d e r L i n e ( ” o r d e r 1 ” , ”RED=TABLE” , 1 3 ) ,
model . O r d e r L i n e ( ” o r d e r 2 ” , ”BLUE=LIPSTICK ” , 1 4 ) ,
]

Copyright © 2021, 2022 Lan Hui 70


a s s e r t s e s s i o n . q u e r y ( model . O r d e r L i n e ) . a l l ( ) == e x p e c t e d

def t e s t o r d e r l i n e m a p p e r c a n s a v e l i n e s ( s e s s i o n ) :
n e w l i n e = model . O r d e r L i n e ( ” o r d e r 1 ” , ”DECORATIVE=WIDGET” , 1 2 )
s e s s i o n . add ( n e w l i n e )
s e s s i o n . commit ( )

rows = l i s t ( s e s s i o n . e x e c u t e ( ’ SELECT o r d e r i d , sku , q t y FROM ” o r d e r


a s s e r t rows == [ ( ” o r d e r 1 ” , ”DECORATIVE=WIDGET” , 1 2 ) ]

Copyright © 2021, 2022 Lan Hui 71


Testing The Repository

We use a more specific repository here: SqlAlchemyRepository.

From session.query(Batch) (the SQLAlchemy language)


to batches repo.get (the repository pattern).

Test add - saving an object


def t e s t r e p o s i t o r y c a n s a v e a b a t c h ( s e s s i o n ) :
b a t c h = model . Batch ( ” b a t c h 1 ” , ”RUSTY=SOAPDISH” , 1 0 0 , e t a=None )

repo = r e p o s i t o r y . SqlAlchemyRepository ( s e s s i o n )
r e p o . add ( b a t c h ) #( 1 )
s e s s i o n . commit ( ) #( 2 )

Copyright © 2021, 2022 Lan Hui 72


rows = s e s s i o n . e x e c u t e ( #( 3 )
’ SELECT r e f e r e n c e , sku , p u r c h a s e d q u a n t i t y , e t a FROM ” b a t c h e s
)
a s s e r t l i s t ( rows ) == [ ( ” b a t c h 1 ” , ”RUSTY=SOAPDISH” , 1 0 0 , None ) ]

Test get - retrieving a complex object.


def i n s e r t o r d e r l i n e ( s e s s i o n ) :
s e s s i o n . e x e c u t e ( #( 1 )
”INSERT INTO o r d e r l i n e s ( o r d e r i d , sku , q t y ) ”
’ VALUES ( ” o r d e r 1 ” , ”GENERIC=SOFA” , 1 2 ) ’
)
[ [ o r d e r l i n e i d ] ] = session . execute (
”SELECT i d FROM o r d e r l i n e s WHERE o r d e r i d =: o r d e r i d AND s k u =: s k
d i c t ( o r d e r i d=” o r d e r 1 ” , s k u=”GENERIC=SOFA” ) ,
)
return o r d e r l i n e i d

Copyright © 2021, 2022 Lan Hui 73


def i n s e r t b a t c h ( s e s s i o n , b a t c h i d ) : #( 2 )
...

def t e s t r e p o s i t o r y c a n r e t r i e v e a b a t c h w i t h a l l o c a t i o n s ( s e s s i o n ) :
orderline id = insert order line ( session )
b a t c h 1 i d = i n s e r t b a t c h ( s e s s i o n , ” batch1 ” )
i n s e r t b a t c h ( s e s s i o n , ” batch2 ” )
i n s e r t a l l o c a t i o n ( s e s s i o n , o r d e r l i n e i d , b a t c h 1 i d ) #( 2 )

repo = r e p o s i t o r y . SqlAlchemyRepository ( s e s s i o n )
r e t r i e v e d = repo . get ( ” batch1 ” )

e x p e c t e d = model . Batch ( ” b a t c h 1 ” , ”GENERIC=SOFA” , 1 0 0 , e t a=None )


a s s e r t r e t r i e v e d == e x p e c t e d # Batch . e q o n l y compares r e f e r e n
#(3)
a s s e r t r e t r i e v e d . s k u == e x p e c t e d . s k u #( 4 )

Copyright © 2021, 2022 Lan Hui 74


a s s e r t r e t r i e v e d . p u r c h a s e d q u a n t i t y == e x p e c t e d . p u r c h a s e d q u a n t i
a s s e r t r e t r i e v e d . a l l o c a t i o n s == { #( 4 )
model . O r d e r L i n e ( ” o r d e r 1 ” , ”GENERIC=SOFA” , 1 2 ) ,
}

SqlAlchemyRepository:
class SqlAlchemyRepository ( AbstractRepository ) :
def i n i t ( self , session ):
self . session = session

def add ( s e l f , b a t c h ) :
s e l f . s e s s i o n . add ( b a t c h )

def g e t ( s e l f , r e f e r e n c e ) :
r e t u r n s e l f . s e s s i o n . q u e r y ( model . Batch ) . f i l t e r b y ( r e f e r e n c e=r e f

def l i s t ( s e l f ) :

Copyright © 2021, 2022 Lan Hui 75


r e t u r n s e l f . s e s s i o n . q u e r y ( model . Batch ) . a l l ( )

Copyright © 2021, 2022 Lan Hui 76


Updated API Endpoint

Not using Repository Pattern:


@flask . route . gubbins
def a l l o c a t e e n d p o i n t ( ) :
session = s t a r t s e s s i o n ()

# e x t r a c t o r d e r l i n e from r e q u e s t
l i n e = OrderLine (
request . json [ ’ orderid ’ ] ,
r e q u e s t . j s o n [ ’ sku ’ ] ,
request . json [ ’ qty ’ ] ,
)

# l o a d a l l b a t c h e s from t h e DB

Copyright © 2021, 2022 Lan Hui 77


b a t c h e s = s e s s i o n . q u e r y ( Batch ) . a l l ( )

# c a l l our domain s e r v i c e
a l l o c a t e ( line , batches )

# save the a l l o c a t i o n back to the database


s e s s i o n . commit ( )

r e t u r n 201

Using Repository Pattern:


@flask . route . gubbins
def a l l o c a t e e n d p o i n t ( ) :
batches = SqlAlchemyRepository . l i s t ()
lines = [
OrderLine ( l [ ’ o r d e r i d ’ ] , l [ ’ sku ’ ] , l [ ’ qty ’ ] )
f o r l i n r e q u e s t . params . . .

Copyright © 2021, 2022 Lan Hui 78


]
a l l o c a t e ( lines , batches )
s e s s i o n . commit ( )
r e t u r n 201

Copyright © 2021, 2022 Lan Hui 79


Coupling and Abstractions

Problem: high-level code is coupled to low-level details.


Tricks:

• Use simple abstractions to hide mess.

• Separate what we want to do from how to do it.

• Make interface between business logic and messy I/O.

Coupling: changing component A may break component B.


Copyright © 2021, 2022 Lan Hui 80
Locally, high coupling is not a bad thing – this means high
cohesion.

Globally, coupling is bad.

Figure 1. Lots of coupling in Chapter 3.

Figure 2. Less coupling in Chapter 3.

Example: directory sync.

Imperative Shell, Functional Core.

Copyright © 2021, 2022 Lan Hui 81


The Service Layer

This layer deals with orchestration logic.


What does the Service Layer pattern look like?
How is it different from entrypoint, i.e., Flask API endpoint?
How is it different from domain model, i.e., business logic?
Flask API talks to the service layer; the service layers talks
to the domain model.
Don’t forget our task: allocate an order line to a batch.
Copyright © 2021, 2022 Lan Hui 82
BTW, the authors faithfully use TDD (test code is even
more than non-test code):

Test code: 11.8 KB


Functional code: 8.7KB
Fast tests (e.g., unit tests) and slow tests (e.g., E2E tests).
Copyright © 2021, 2022 Lan Hui 83
Figure 2. The service layer will become the main way into
our app.

What we have now:

• Domain model.

• Domain service (allocate).

• Repsitory interface.
@ p y t e s t . mark . u s e f i x t u r e s ( ” r e s t a r t a p i ” )
def t e s t a p i r e t u r n s a l l o c a t i o n ( a d d s t o c k ) :
sku , o t h e r s k u = r a n d o m s k u ( ) , r a n d o m s k u ( ” o t h e r ” ) #( 1 )
earlybatch = random batchref (1)

Copyright © 2021, 2022 Lan Hui 84


laterbatch = random batchref (2)
otherbatch = random batchref (3)
a d d s t o c k ( #( 2 )
[
( l a t e r b a t c h , sku , 1 0 0 , ”2011 = 01 = 02” ) ,
( e a r l y b a t c h , sku , 1 0 0 , ”2011 = 01 = 01” ) ,
( o t h e r b a t c h , o t h e r s k u , 1 0 0 , None ) ,
]
)
d a t a = { ” o r d e r i d ” : r a n d o m o r d e r i d ( ) , ” s k u ” : sku , ” q t y ” : 3 }
u r l = c o n f i g . g e t a p i u r l ( ) #( 3 )

r = r e q u e s t s . p o s t ( f ” { u r l } / a l l o c a t e ” , j s o n=d a t a )

a s s e r t r . s t a t u s c o d e == 201
a s s e r t r . j s o n ( ) [ ” b a t c h r e f ” ] == e a r l y b a t c h

Copyright © 2021, 2022 Lan Hui 85


@ p y t e s t . mark . u s e f i x t u r e s ( ” r e s t a r t a p i ” )
def t e s t a l l o c a t i o n s a r e p e r s i s t e d ( a d d s t o c k ) :
sku = random sku ( )
batch1 , b a t c h 2 = r a n d o m b a t c h r e f ( 1 ) , r a n d o m b a t c h r e f ( 2 )
order1 , order2 = random orderid (1) , random orderid (2)
add stock (
[ ( batch1 , sku , 1 0 , ”2011 = 01 = 01” ) , ( batch2 , sku , 1 0 , ”2011 = 01 = 0
)
l i n e 1 = { ” o r d e r i d ” : o r d e r 1 , ” s k u ” : sku , ” q t y ” : 10 }
l i n e 2 = { ” o r d e r i d ” : o r d e r 2 , ” s k u ” : sku , ” q t y ” : 10 }
url = config . g e t a p i u r l ()

# f i r s t o r d e r u s e s up a l l s t o c k i n b a t c h 1
r = r e q u e s t s . p o s t ( f ” { u r l } / a l l o c a t e ” , j s o n=l i n e 1 )
a s s e r t r . s t a t u s c o d e == 201
a s s e r t r . j s o n ( ) [ ” b a t c h r e f ” ] == b a t c h 1

Copyright © 2021, 2022 Lan Hui 86


# second o r d e r s h o u l d go t o b a t c h 2
r = r e q u e s t s . p o s t ( f ” { u r l } / a l l o c a t e ” , j s o n=l i n e 2 )
a s s e r t r . s t a t u s c o d e == 201
a s s e r t r . j s o n ( ) [ ” b a t c h r e f ” ] == b a t c h 2

@ p y t e s t . mark . u s e f i x t u r e s ( ” r e s t a r t a p i ” )
def t e s t 4 0 0 m e s s a g e f o r o u t o f s t o c k ( a d d s t o c k ) : #( 1 )
sku , s m a l L b a t c h , l a r g e o r d e r = r a n d o m s k u ( ) , r a n d o m b a t c h r e f ( ) , r
add stock (
[ ( s m a l L b a t c h , sku , 1 0 , ”2011 = 01 = 01” ) , ]
)
d a t a = { ” o r d e r i d ” : l a r g e o r d e r , ” s k u ” : sku , ” q t y ” : 20 }
url = config . g e t a p i u r l ()
r = r e q u e s t s . p o s t ( f ” { u r l } / a l l o c a t e ” , j s o n=d a t a )
a s s e r t r . s t a t u s c o d e == 400

Copyright © 2021, 2022 Lan Hui 87


a s s e r t r . j s o n ( ) [ ” message ” ] == f ”Out o f s t o c k f o r s k u { s k u } ”

@ p y t e s t . mark . u s e f i x t u r e s ( ” r e s t a r t a p i ” )
def t e s t 4 0 0 m e s s a g e f o r i n v a l i d s k u ( ) : #( 2 )
unknown sku , o r d e r i d = r a n d o m s k u ( ) , r a n d o m o r d e r i d ( )
d a t a = { ” o r d e r i d ” : o r d e r i d , ” s k u ” : unknown sku , ” q t y ” : 20 }
url = config . g e t a p i u r l ()
r = r e q u e s t s . p o s t ( f ” { u r l } / a l l o c a t e ” , j s o n=d a t a )
a s s e r t r . s t a t u s c o d e == 400
a s s e r t r . j s o n ( ) [ ” message ” ] == f ” I n v a l i d s k u { unknown sku } ”

@pytest.mark.usefixtures(“restart api”): call restart api


in conftest.py before carrying out the test.
from f l a s k import F l a s k , r e q u e s t
from s q l a l c h e m y import c r e a t e e n g i n e

Copyright © 2021, 2022 Lan Hui 88


from s q l a l c h e m y . orm import s e s s i o n m a k e r

import config
import model
import orm
import repository

orm . s t a r t m a p p e r s ( )
g e t s e s s i o n = s e s s i o n m a k e r ( b i n d=c r e a t e e n g i n e ( c o n f i g . g e t p o s t g r e s u r i (
app = F l a s k ( n a m e )

def i s v a l i d s k u ( sku , b a t c h e s ) :
r e t u r n s k u i n {b . s k u f o r b i n b a t c h e s }

@app . r o u t e ( ” / a l l o c a t e ” , methods =[”POST” ] )

Copyright © 2021, 2022 Lan Hui 89


def a l l o c a t e e n d p o i n t ( ) :
session = get session ()
batches = r e p o s i t o r y . SqlAlchemyRepository ( s e s s i o n ) . l i s t ()
l i n e = model . O r d e r L i n e (
r e q u e s t . j s o n [ ” o r d e r i d ” ] , r e q u e s t . j s o n [ ” sku ” ] , r e q u e s t . j s o n [ ” qt
)

i f not i s v a l i d s k u ( l i n e . sku , b a t c h e s ) :
r e t u r n { ” message ” : f ” I n v a l i d s k u { l i n e . s k u } ” } , 400

try :
b a t c h r e f = model . a l l o c a t e ( l i n e , b a t c h e s )
except model . OutOfStock a s e :
r e t u r n { ” message ” : s t r ( e ) } , 400

s e s s i o n . commit ( )
r e t u r n { ” b a t c h r e f ” : b a t c h r e f } , 201

Copyright © 2021, 2022 Lan Hui 90


Consequence: inverted test pyramid (ice-cream cone model).

Solution: introduce a service layer and put orchestration


logic in this layer.

Steps:

• Fetch some objects from the repository.

• Make some checks.

• Call a domain service.

• Save/update any state that has been changed.


Copyright © 2021, 2022 Lan Hui 91
class InvalidSku ( Exception ) :
pass

def i s v a l i d s k u ( sku , b a t c h e s ) :
r e t u r n s k u i n {b . s k u f o r b i n b a t c h e s }

def a l l o c a t e ( l i n e : O r d e r L i n e , r e p o : A b s t r a c t R e p o s i t o r y , s e s s i o n ) => st
b a t c h e s = r e p o . l i s t ( ) #( 1 )
i f not i s v a l i d s k u ( l i n e . sku , b a t c h e s ) : #( 2 )
r a i s e I n v a l i d S k u ( f ” I n v a l i d s k u { l i n e . s k u }” )
b a t c h r e f = model . a l l o c a t e ( l i n e , b a t c h e s ) #( 3 )
s e s s i o n . commit ( ) #( 4 )
return b a t c h r e f

Two awkwardness in the service layer: (1) Coupled to


Copyright © 2021, 2022 Lan Hui 92
OrderLine. (2) Coupled to session.
Depend on Abstractions: def allocate(line:
OrderLine, repo: AbstractRepository, session)
Let the Flask API endpoint do “web stuff” only.
@app . r o u t e ( ” / a l l o c a t e ” , methods =[”POST” ] )
def a l l o c a t e e n d p o i n t ( ) :
s e s s i o n = g e t s e s s i o n ( ) #( 1 )
r e p o = r e p o s i t o r y . S q l A l c h e m y R e p o s i t o r y ( s e s s i o n ) #( 1 )
l i n e = model . O r d e r L i n e (
r e q u e s t . j s o n [ ” o r d e r i d ” ] , r e q u e s t . j s o n [ ” sku ” ] , r e q u e s t . j s o n [ ” qt
#( 2 )
)

try :
b a t c h r e f = s e r v i c e s . a l l o c a t e ( l i n e , re po , s e s s i o n ) #( 2 )

Copyright © 2021, 2022 Lan Hui 93


except ( model . OutOfStock , s e r v i c e s . I n v a l i d S k u ) a s e :
r e t u r n { ” message ” : s t r ( e ) } , 400 ( 3 )

r e t u r n { ” b a t c h r e f ” : b a t c h r e f } , 201 (3)

E2E tests: happy path and unhappy path.


@ p y t e s t . mark . u s e f i x t u r e s ( ” r e s t a r t a p i ” )
def t e s t h a p p y p a t h r e t u r n s 2 0 1 a n d a l l o c a t e d b a t c h ( a d d s t o c k ) :
sku , o t h e r s k u = r a n d o m s k u ( ) , r a n d o m s k u ( ” o t h e r ” )
earlybatch = random batchref (1)
laterbatch = random batchref (2)
otherbatch = random batchref (3)
add stock (
[
( l a t e r b a t c h , sku , 1 0 0 , ”2011 = 01 = 02” ) ,
( e a r l y b a t c h , sku , 1 0 0 , ”2011 = 01 = 01” ) ,
( o t h e r b a t c h , o t h e r s k u , 1 0 0 , None ) ,

Copyright © 2021, 2022 Lan Hui 94


]
)
d a t a = { ” o r d e r i d ” : r a n d o m o r d e r i d ( ) , ” s k u ” : sku , ” q t y ” : 3 }
url = config . g e t a p i u r l ()

r = r e q u e s t s . p o s t ( f ” { u r l } / a l l o c a t e ” , j s o n=d a t a )

a s s e r t r . s t a t u s c o d e == 201
a s s e r t r . j s o n ( ) [ ” b a t c h r e f ” ] == e a r l y b a t c h

@ p y t e s t . mark . u s e f i x t u r e s ( ” r e s t a r t a p i ” )
def t e s t u n h a p p y p a t h r e t u r n s 4 0 0 a n d e r r o r m e s s a g e ( ) :
unknown sku , o r d e r i d = r a n d o m s k u ( ) , r a n d o m o r d e r i d ( )
d a t a = { ” o r d e r i d ” : o r d e r i d , ” s k u ” : unknown sku , ” q t y ” : 20 }
url = config . g e t a p i u r l ()
r = r e q u e s t s . p o s t ( f ” { u r l } / a l l o c a t e ” , j s o n=d a t a )
a s s e r t r . s t a t u s c o d e == 400

Copyright © 2021, 2022 Lan Hui 95


a s s e r t r . j s o n ( ) [ ” message ” ] == f ” I n v a l i d s k u { unknown sku } ”

Service layer vs. domain service. For example, where should


we put the responsiblity of Calculating Tax?

Copyright © 2021, 2022 Lan Hui 96


Creating and Organizing Folders to Show
Architecture

Check Putting Things in Folders to See Where It All Belongs.

Copyright © 2021, 2022 Lan Hui 97


Unit of Work Pattern

UoW (pronounced you-wow): Unit of Work


Without UoW:
Check Figure 1. Without UoW: API talks directly to three
layers

• The Flask API talks directly to the database layer to start a


session.

• The Flask API talks to the repository layer to initialize


Copyright © 2021, 2022 Lan Hui 98
SQLAlchemyRepository.

• The Flask API talks to the service layer to ask it to allocate.

With UoW:
Check Figure 2. With UoW: UoW now manages database
state
Benefit: Decouple service layer from the data layer.

• The Flask API initializes a unit of work.

• The Flask API invokes a service.


Copyright © 2021, 2022 Lan Hui 99
Repository as a collaborator class for UnitOfWork.

service layer/services.py:
def a l l o c a t e (
o r d e r i d : str , sku : str , qty : int ,
uow : u n i t o f w o r k . A b s t r a c t U n i t O f W o r k ,
) => s t r :
l i n e = O r d e r L i n e ( o r d e r i d , sku , q t y )
w i t h uow : #( 1 )
b a t c h e s = uow . b a t c h e s . l i s t ( ) #( 2 )
...
b a t c h r e f = model . a l l o c a t e ( l i n e , b a t c h e s )
uow . commit ( ) #( 3 )

Use uow as a context manager (see this tutorial for more


explanation on Context Managers).
Copyright © 2021, 2022 Lan Hui 100
uow.batches gives us a repo.

integration/test uow.py
def t e s t u o w c a n r e t r i e v e a b a t c h a n d a l l o c a t e t o i t ( s e s s i o n f a c t o r y ) :
session = session factory ()
i n s e r t b a t c h ( s e s s i o n , ” b a t c h 1 ” , ”HIPSTER=WORKBENCH” , 1 0 0 , None )
s e s s i o n . commit ( )

uow = u n i t o f w o r k . SqlAlchemyUnitOfWork ( s e s s i o n f a c t o r y ) #( 1 )
w i t h uow :
b a t c h = uow . b a t c h e s . g e t ( r e f e r e n c e=” b a t c h 1 ” ) #( 2 )
l i n e = model . O r d e r L i n e ( ” o1 ” , ”HIPSTER=WORKBENCH” , 1 0 )
batch . a l l o c a t e ( l i n e )
uow . commit ( ) #( 3 )

b a t c h r e f = g e t a l l o c a t e d b a t c h r e f ( s e s s i o n , ” o1 ” , ”HIPSTER=WORKBEN
a s s e r t b a t c h r e f == ” b a t c h 1 ”

Copyright © 2021, 2022 Lan Hui 101


Now define UoW
c l a s s A b s t r a c t U n i t O f W o r k ( abc . ABC ) :
batches : r e p o s i t o r y . AbstractRepository #( 1 )

def e x i t ( self , * args ): #( 2 )


s e l f . r o l l b a c k ( ) #( 4 )

@abc . a b s t r a c t m e t h o d
def commit ( s e l f ) : #( 3 )
ra is e NotImplementedError

@abc . a b s t r a c t m e t h o d
def r o l l b a c k ( s e l f ) : #( 4 )
ra is e NotImplementedError

Copyright © 2021, 2022 Lan Hui 102


DEFAULT SESSION FACTORY = s e s s i o n m a k e r ( #( 1 )
b i n d=c r e a t e e n g i n e (
config . get postgres uri () ,
)
)

c l a s s SqlAlchemyUnitOfWork ( A b s t r a c t U n i t O f W o r k ) :
def i n i t ( s e l f , s e s s i o n f a c t o r y=DEFAULT SESSION FACTORY ) :
s e l f . s e s s i o n f a c t o r y = s e s s i o n f a c t o r y #( 1 )

def enter ( self ):


s e l f . session = s e l f . session factory () # type : Session
#(2)
s e l f . batches = r e p o s i t o r y . SqlAlchemyRepository ( s e l f . s e s s i o n )
#( 2 )
r e t u r n super ( ) . enter ()

Copyright © 2021, 2022 Lan Hui 103


def e x i t ( self , * args ):
super ( ) . e x i t (* args )
s e l f . s e s s i o n . c l o s e ( ) #( 3 )

def commit ( s e l f ) : #( 4 )
s e l f . s e s s i o n . commit ( )

def r o l l b a c k ( s e l f ) : #( 4 )
s e l f . session . rollback ()

Use the UoW in the service layer


def a d d b a t c h (
r e f : str , sku : str , qty : int , eta : O p t i o n a l [ date ] ,
uow : u n i t o f w o r k . A b s t r a c t U n i t O f W o r k , #( 1 )
):
w i t h uow :

Copyright © 2021, 2022 Lan Hui 104


uow . b a t c h e s . add ( model . Batch ( r e f , sku , qty , e t a ) )
uow . commit ( )

def a l l o c a t e (
o r d e r i d : str , sku : str , qty : int ,
uow : u n i t o f w o r k . A b s t r a c t U n i t O f W o r k , #( 1 )
) => s t r :
l i n e = O r d e r L i n e ( o r d e r i d , sku , q t y )
w i t h uow :
b a t c h e s = uow . b a t c h e s . l i s t ( )
i f not i s v a l i d s k u ( l i n e . sku , b a t c h e s ) :
r a i s e I n v a l i d S k u ( f ” I n v a l i d s k u { l i n e . s k u }” )
b a t c h r e f = model . a l l o c a t e ( l i n e , b a t c h e s )
uow . commit ( )
return b a t c h r e f

Copyright © 2021, 2022 Lan Hui 105


Aggregate Pattern

A collection of entities (domain objects) treated a unit for


data changes. Example: a shopping cart (encapsulating many
items).
An aggregate provides a single Consistency Boundary. A
shopping cart provides a consistency boundary. We do not
change the shopping carts of several customers at the same
time. Cart is the accesible root entity, while other entities are
not directly accessible from outside.
Example: Aggregates with multiple or single entities
Copyright © 2021, 2022 Lan Hui 106
Without any design patterns: CSV over SMTP / spreadsheet
over email. Low initial complexity. Does not scale well.

Invariants – things that must be true after we finish an


operation.

Constraints – double-booking is not allowed.

Business rules for order line allocation: 1 and 2.

Concurrency and locks: allocating 360,000 order lines per


hour. How many order lines per second? Too slow if we lock
the allocations table while allocating each order line.

Not OK: allocate multiple order lines for the same product
Copyright © 2021, 2022 Lan Hui 107
DEADLY-SPOON at the same time.

OK: allocate multiple order lines for different products at


the same time.

Bounded Context:

• Product(sku, batches) for allocation service.

• Product(sku, description, price, image url) for


ecommerce.

Define the aggregate Product:


Copyright © 2021, 2022 Lan Hui 108
c l a s s Product :
def i n i t ( s e l f , s k u : s t r , b a t c h e s : L i s t [ Batch ] ) :
s e l f . s k u = s k u #( 1 )
s e l f . b a t c h e s = b a t c h e s #( 2 )

def a l l o c a t e ( s e l f , l i n e : O r d e r L i n e ) => s t r : #( 3 )
try :
b a t c h = next ( b f o r b i n s o r t e d ( s e l f . b a t c h e s ) i f b . c a n a l l o
batch . a l l o c a t e ( l i n e )
return batch . r e f e r e n c e
except S t o p I t e r a t i o n :
r a i s e OutOfStock ( f ”Out o f s t o c k f o r s k u { l i n e . s k u } ” )

What has changed in the repository? The get method.


class SqlAlchemyRepository ( AbstractRepository ) :
def i n i t ( self , session ):
self . session = session

Copyright © 2021, 2022 Lan Hui 109


def add ( s e l f , p r o d u c t ) :
s e l f . s e s s i o n . add ( p r o d u c t )

def g e t ( s e l f , s k u ) :
r e t u r n s e l f . s e s s i o n . q u e r y ( model . P r o d u c t ) . f i l t e r b y ( s k u=s k u ) . f i

How does unit of work look like now?


c l a s s SqlAlchemyUnitOfWork ( A b s t r a c t U n i t O f W o r k ) :
def i n i t ( s e l f , s e s s i o n f a c t o r y=DEFAULT SESSION FACTORY ) :
self . session factory = session factory

def enter ( self ):


s e l f . s e s s i o n = s e l f . s e s s i o n f a c t o r y () # type : Session
s e l f . products = r e p o s i t o r y . SqlAlchemyRepository ( s e l f . s e s s i o n )
r e t u r n super ( ) . e n t e r ( )

Copyright © 2021, 2022 Lan Hui 110


def e x i t ( self , * args ):
super ( ) . e x i t (* args )
s e l f . session . close ()

def commit ( s e l f ) :
s e l f . s e s s i o n . commit ( )

def r o l l b a c k ( s e l f ) :
s e l f . session . rollback ()

How does service allocate look like now?


def a d d b a t c h (
r e f : str , sku : str , qty : int , eta : O p t i o n a l [ date ] ,
uow : u n i t o f w o r k . A b s t r a c t U n i t O f W o r k ,
):
w i t h uow :
p r o d u c t = uow . p r o d u c t s . g e t ( s k u=s k u )

Copyright © 2021, 2022 Lan Hui 111


i f p r o d u c t i s None :
p r o d u c t = model . P r o d u c t ( sku , b a t c h e s = [ ] )
uow . p r o d u c t s . add ( p r o d u c t )
p r o d u c t . b a t c h e s . append ( model . Batch ( r e f , sku , qty , e t a ) )
uow . commit ( )

def a l l o c a t e (
o r d e r i d : str , sku : str , qty : int ,
uow : u n i t o f w o r k . A b s t r a c t U n i t O f W o r k ,
) => s t r :
l i n e = O r d e r L i n e ( o r d e r i d , sku , q t y )
w i t h uow :
p r o d u c t = uow . p r o d u c t s . g e t ( s k u= l i n e . s k u )
i f p r o d u c t i s None :
r a i s e I n v a l i d S k u ( f ” I n v a l i d s k u { l i n e . s k u }” )
batchref = product . a l l o c a t e ( l i n e )
uow . commit ( )

Copyright © 2021, 2022 Lan Hui 112


return b a t c h r e f

Copyright © 2021, 2022 Lan Hui 113


Domain Events Pattern & Message Bus
Pattern

Task: send out an email alert when allocation is failed due


to out of stock.
Possible solutions:

• Dump the email sending stuff in flask app.py, the HTTP


layer (the endpoint). Quick and dirty. See function
send mail.
@app . r o u t e ( ” / a l l o c a t e ” , methods =[”POST” ] )

Copyright © 2021, 2022 Lan Hui 114


def a l l o c a t e e n d p o i n t ( ) :
l i n e = model . O r d e r L i n e (
request . json [ ” orderid ” ] ,
r e q u e s t . j s o n [ ” sku ” ] ,
request . json [ ” qty ” ] ,
)
try :
uow = u n i t o f w o r k . SqlAlchemyUnitOfWork ( )
b a t c h r e f = s e r v i c e s . a l l o c a t e ( l i n e , uow )
except ( model . OutOfStock , s e r v i c e s . I n v a l i d S k u ) a s e :
send mail (
” out of s t o c k ” ,
” stock admin@made . com” ,
f ”{ l i n e . o r d e r i d } = { l i n e . s k u }”
)
r e t u r n { ” message ” : s t r ( e ) } , 400

r e t u r n { ” b a t c h r e f ” : b a t c h r e f } , 201

Copyright © 2021, 2022 Lan Hui 115


Question: Should the HTTP layer be responsible for sending
out alert email messages? This may violate the Thin Web
Controller Principle, as well violate Single Responsibility
Principle (SRP). Also, how to do unit testing?

• Move the email sending stuff to the domain layer, i.e.,


model.py.
Question: Is the domain layer for sending email, or for
allocating an order line?

• Move the email sending stuff to the service layer, i.e.,


services.py.
def a l l o c a t e (

Copyright © 2021, 2022 Lan Hui 116


o r d e r i d : str , sku : str , qty : int ,
uow : u n i t o f w o r k . A b s t r a c t U n i t O f W o r k ,
) => s t r :
l i n e = O r d e r L i n e ( o r d e r i d , sku , q t y )
w i t h uow :
p r o d u c t = uow . p r o d u c t s . g e t ( s k u= l i n e . s k u )
i f p r o d u c t i s None :
r a i s e I n v a l i d S k u ( f ” I n v a l i d s k u { l i n e . s k u }” )
try :
batchref = product . a l l o c a t e ( l i n e )
uow . commit ( )
return b a t c h r e f
except model . OutOfStock :
e m a i l . s e n d m a i l ( ” stock@made . com” , f ”Out o f s t o c k f o r { l i
raise

allocate or allocate and send mail if out of stock?


Copyright © 2021, 2022 Lan Hui 117
Rule of thumb: one function for one thing.
Where does the responsibility of sending alert belong?
Message Bus!

• Make an Event class for the Out of Stock event. Check


domain/events.py.
from d a t a c l a s s e s import d a t a c l a s s

c l a s s Event : #( 1 )
pass

@dataclass
c l a s s OutOfStock ( E v e n t ) : #( 2 )

Copyright © 2021, 2022 Lan Hui 118


sku : s t r

An event becomes a value object. Do you still remember that


OutOfStock is an exception class in the previous chapters?

• Add an attribute called events in class Product.

• Append this event object in events – raise the event.

Write test before modifying model.py and other files.


def t e s t r e c o r d s o u t o f s t o c k e v e n t i f c a n n o t a l l o c a t e ( ) :
b a t c h = Batch ( ” b a t c h 1 ” , ”SMALL=FORK” , 1 0 , e t a=t o d a y )
p r o d u c t = P r o d u c t ( s k u=”SMALL=FORK” , b a t c h e s =[ b a t c h ] )
p r o d u c t . a l l o c a t e ( O r d e r L i n e ( ” o r d e r 1 ” , ”SMALL=FORK” , 1 0 ) )

Copyright © 2021, 2022 Lan Hui 119


a l l o c a t i o n = p r o d u c t . a l l o c a t e ( O r d e r L i n e ( ” o r d e r 2 ” , ”SMALL=FORK” , 1 )
a s s e r t p r o d u c t . e v e n t s [ = 1] == e v e n t s . OutOfStock ( s k u=”SMALL=FORK” )
#( 1 )
a s s e r t a l l o c a t i o n i s None

Note: product.events[-1].
Now let’s update model.py.
from . import e v e n t s

c l a s s Product :
def i n i t ( s e l f , s k u : s t r , b a t c h e s : L i s t [ Batch ] , v e r s i o n n u m b e r :
s e l f . sku = sku
s e l f . batches = batches
s e l f . version number = version number
s e l f . e v e n t s = [ ] # t y p e : L i s t [ e v e n t s . Event ]

Copyright © 2021, 2022 Lan Hui 120


def a l l o c a t e ( s e l f , l i n e : O r d e r L i n e ) => s t r :
try :
b a t c h = next ( b f o r b i n s o r t e d ( s e l f . b a t c h e s ) i f b . c a n a l l o
batch . a l l o c a t e ( l i n e )
s e l f . v e r s i o n n u m b e r += 1
return batch . r e f e r e n c e
except S t o p I t e r a t i o n :
s e l f . e v e n t s . append ( e v e n t s . OutOfStock ( l i n e . s k u ) )
r e t u r n None

Note: we no longer raise the OutOfStock exception now.

Copyright © 2021, 2022 Lan Hui 121


Message Bus

Purpose: to provide a unified way of invoking use cases from


the endpoint.
Check Figure 1. Events flowing through the system in
Chapter 8 of the textbook.
Method: mapping an event to a list of handlers (function
names).
It is a dictionary.
A message bus has an important function called handle.
Copyright © 2021, 2022 Lan Hui 122
HANDLERS = {
e v e n t s . OutOfStock : [ s e n d o u t o f s t o c k n o t i f i c a t i o n ] ,
} # t y p e : D i c t [ Type [ e v e n t s . Event ] , L i s t [ C a l l

def h a n d l e ( e v e n t : e v e n t s . E v e n t ) :
f o r h a n d l e r i n HANDLERS [ type ( e v e n t ) ] :
handler ( event )

def s e n d o u t o f s t o c k n o t i f i c a t i o n ( e v e n t : e v e n t s . OutOfStock ) :
email . send mail (
” stock@made . com” ,
f ”Out o f s t o c k f o r { e v e n t . s k u } ” ,
)

HANDLERS is a dictionary. Its key is an event type, and its


Copyright © 2021, 2022 Lan Hui 123
value is a list of functions that will do something for that type
of event.

In which folder should we put messagebus.py?

Where will the function handle be called?

Copyright © 2021, 2022 Lan Hui 124


Let Service Layer Publish Events to the
Message Bus

services.py:
from . import m e s s a g e b u s
...

def a l l o c a t e (
o r d e r i d : str , sku : str , qty : int ,
uow : u n i t o f w o r k . A b s t r a c t U n i t O f W o r k ,
) => s t r :
l i n e = O r d e r L i n e ( o r d e r i d , sku , q t y )
w i t h uow :
p r o d u c t = uow . p r o d u c t s . g e t ( s k u= l i n e . s k u )
i f p r o d u c t i s None :

Copyright © 2021, 2022 Lan Hui 125


r a i s e I n v a l i d S k u ( f ” I n v a l i d s k u { l i n e . s k u }” )
t r y : #( 1 )
batchref = product . a l l o c a t e ( l i n e )
uow . commit ( )
return b a t c h r e f
f i n a l l y : #( 1 )
m e s s a g e b u s . h a n d l e ( p r o d u c t . e v e n t s ) #( 2 )

Copyright © 2021, 2022 Lan Hui 126


Let Service Layer Raise Events

services.py:
def a l l o c a t e (
o r d e r i d : str , sku : str , qty : int ,
uow : u n i t o f w o r k . A b s t r a c t U n i t O f W o r k ,
) => s t r :
l i n e = O r d e r L i n e ( o r d e r i d , sku , q t y )
w i t h uow :
p r o d u c t = uow . p r o d u c t s . g e t ( s k u= l i n e . s k u )
i f p r o d u c t i s None :
r a i s e I n v a l i d S k u ( f ” I n v a l i d s k u { l i n e . s k u }” )
batchref = product . a l l o c a t e ( l i n e )
uow . commit ( ) #( 1 )

Copyright © 2021, 2022 Lan Hui 127


i f b a t c h r e f i s None :
m e s s a g e b u s . h a n d l e ( e v e n t s . OutOfStock ( l i n e . s k u ) )
return b a t c h r e f

Copyright © 2021, 2022 Lan Hui 128


Let UoW Publish Events to the Message Bus

... and message bus handles them.


from . import m e s s a g e b u s

c l a s s A b s t r a c t U n i t O f W o r k ( abc . ABC ) :
products : repository . AbstractRepository

def enter ( self ) => AbstractUnitOfWork :


return s e l f

def e x i t ( self , * args ):


s e l f . rollback ()

Copyright © 2021, 2022 Lan Hui 129


def commit ( s e l f ) :
s e l f . commit ( )
s e l f . publish events ()

def p u b l i s h e v e n t s ( s e l f ) :
for product in s e l f . products . seen :
while product . events :
e v e n t = p r o d u c t . e v e n t s . pop ( 0 )
messagebus . handle ( event )

@abc . a b s t r a c t m e t h o d
def commit ( s e l f ) :
ra is e NotImplementedError

@abc . a b s t r a c t m e t h o d
def r o l l b a c k ( s e l f ) :
ra is e NotImplementedError

Copyright © 2021, 2022 Lan Hui 130


Note: products is a repository object. product is a
Product object which contains a list of batches.

Question: how to understand the seen in repostory.py?

Magic. Most complex design as the authors said!

Copyright © 2021, 2022 Lan Hui 131


Event Storming

Treat events as first-class citizens.

• BatchCreated

• BatchQuantityChanged

• AllocationRequired

• OutOfStock
Copyright © 2021, 2022 Lan Hui 132
Each of the above events can be denoted as a dataclass.

How does the endpoint flask app.py look like now?

# add batch
event = events.BatchCreated(
request.json["ref"],
request.json["sku"],
request.json["qty"], eta
)

messagebus.handle(event, unit_of_work.SqlAlchemyUnitOfWork())

# allocate order line


event = events.AllocationRequired(

Copyright © 2021, 2022 Lan Hui 133


request.json["orderid"],
request.json["sku"],
request.json["qty"]
)

messagebus.handle(event, unit_of_work.SqlAlchemyUnitOfWork())

Copyright © 2021, 2022 Lan Hui 134


From Services to Event Handlers

Convert use-case functions to event handlers.


services.py is replaced by messagebus.py.
def h a n d l e (
e v e n t : e v e n t s . Event ,
uow : u n i t o f w o r k . A b s t r a c t U n i t O f W o r k ,
):
results = []
queue = [ e v e n t ]
w h i l e queue :
e v e n t = queue . pop ( 0 )
f o r h a n d l e r i n HANDLERS [ type ( e v e n t ) ] :
r e s u l t s . append ( h a n d l e r ( e v e n t , uow=uow ) )

Copyright © 2021, 2022 Lan Hui 135


queue . e x t e n d ( uow . c o l l e c t n e w e v e n t s ( ) )
return r e s u l t s

HANDLERS = {
events . BatchCreated : [ h a n d l e r s . add batch ] ,
e v e n t s . BatchQuantityChanged : [ h a n d l e r s . c h a n g e b a t c h q u a n t i t y ] ,
events . AllocationRequired : [ handlers . allocate ] ,
e v e n t s . OutOfStock : [ h a n d l e r s . s e n d o u t o f s t o c k n o t i f i c a t i o n ] ,
} # t y p e : D i c t [ Type [ e v e n t s . Event ] , L i s t [ C a l l a b l e ] ]

Question: what does uow.collect new events() do?


Handle a series of old and new events using handler.
Use a handler to handle an incoming event. This handler
could make another event, which is to be handled by another
Copyright © 2021, 2022 Lan Hui 136
handler.

A possibility of circular dependency?

Example: the event BatchQuantityChanged is handled by


change batch quantity, which could in turn make an event
called AllocationRequired.
change batch quantity in model.py.
class Product:
def __init__(self, sku: str, batches: List[Batch], version_number: int = 0):
self.sku = sku
self.batches = batches
self.version_number = version_number
self.events = [] # type: List[events.Event]

Copyright © 2021, 2022 Lan Hui 137


def allocate(self, line: OrderLine) -> str:
try:
batch = next(b for b in sorted(self.batches) if b.can_allocate(line)
batch.allocate(line)
self.version_number += 1
return batch.reference
except StopIteration:
self.events.append(events.OutOfStock(line.sku))
return None

def change_batch_quantity(self, ref: str, qty: int):


batch = next(b for b in self.batches if b.reference == ref)
batch._purchased_quantity = qty
while batch.available_quantity < 0:
line = batch.deallocate_one()
self.events.append( ##########################################
events.AllocationRequired(line.orderid, line.sku, line.qty)
)

Copyright © 2021, 2022 Lan Hui 138


Message Processor

Now the service layer becomes an event processor.

Before: uow pushes events onto the message bus.

Now: the message bus pulls events from the uow.

Copyright © 2021, 2022 Lan Hui 139


Commands and Command Handlers

BatchCreated versus CreateBatch.

BatchCreated - event (broadcast knowledge, for


notification).

CreateBatch - command (capture intent).

Examples:
class Command:
pass

Copyright © 2021, 2022 Lan Hui 140


@dataclass
class CreateBatch(Command): #(2)
ref: str
sku: str
qty: int
eta: Optional[date] = None

@dataclass
class Allocate(Command): #(1)
orderid: str
sku: str
qty: int

@dataclass
class ChangeBatchQuantity(Command): #(3)

Copyright © 2021, 2022 Lan Hui 141


ref: str
qty: int

Separate two kinds of handlers, EVENT HANDLERS and


COMMAND HANDLERS:
EVENT HANDLERS = {
e v e n t s . OutOfStock : [ h a n d l e r s . s e n d o u t o f s t o c k n o t i f i c a t i o n ] ,
} # t y p e : D i c t [ Type [ e v e n t s . Event ] , L i s t [ C a l l a b l e ] ]

COMMAND HANDLERS = {
commands . A l l o c a t e : h a n d l e r s . a l l o c a t e ,
commands . C r e a t e B a t c h : h a n d l e r s . a d d b a t c h ,
commands . C h a n g e B a t c h Q u a n t i t y : h a n d l e r s . c h a n g e b a t c h q u a n t i t y ,
} # t y p e : D i c t [ Type [ commands . Command ] , C a l l a b l e ]

The message bus should be adpated with the commands.


Copyright © 2021, 2022 Lan Hui 142
We should keep the important function handle. Now we use
the name message instead of event.
Message = Union [ commands . Command , e v e n t s . E v e n t ]

def h a n d l e ( #( 1 )
message : Message ,
uow : u n i t o f w o r k . A b s t r a c t U n i t O f W o r k ,
):
results = []
queue = [ message ]
w h i l e queue :
message = queue . pop ( 0 )
i f i s i n s t a n c e ( message , e v e n t s . E v e n t ) :
h a n d l e e v e n t ( message , queue , uow ) #( 2 )
e l i f i s i n s t a n c e ( message , commands . Command ) :
c m d r e s u l t = handle command ( message , queue , uow ) #( 2 )

Copyright © 2021, 2022 Lan Hui 143


r e s u l t s . append ( c m d r e s u l t )
else :
r a i s e E x c e p t i o n ( f ” { message } was n o t an E v e n t o r Command” )
return r e s u l t s

Copyright © 2021, 2022 Lan Hui 144


Test-driven Development

It forces writing tests before writing code.


It helps understand requirements better.
A good reading: Yamaura, Tsuneo. ”How to Design
Practical Test Cases.” IEEE Software (November/December
1998): 30-36.
It ensures that code continues working after we make
changes.
Note that changes to one part of the code can inadvertently
Copyright © 2021, 2022 Lan Hui 145
break other parts.

Copyright © 2021, 2022 Lan Hui 146


Dependency Injection

Related concepts: IoC (Inversion of Control), DIP


(dependency inversion principle), interfaces.

Class A uses class B. Without dependency injection (DI), we


create an object of class B, b, inside class A and use b’s useful
methods. Class A is responsible for creating b.

With DI, instead of creating an object of class B inside


class A, create b outside, and pass this object to class A’s
initialization method.
Copyright © 2021, 2022 Lan Hui 147
If class A depends on object b (i.e., class A’s dependency is
b), then Inject b to class A through the initialization method.
Class A is no longer responsible for creating b.
Why is dependency injection good? Loose coupling. We
can inject any object derived from a subclass of the Abstract
Base Class B to class A. More flexible. Easier to test.
Example: the allocate’s second argument (repo)’s type is
Abstract Base Class. We can pass a repo of FakeRepository,
or a repo of SqlAlchemyRepository.
# from s e r v i c e s . py i n Chapter 4
def a l l o c a t e ( l i n e : O r d e r L i n e , r e p o : A b s t r a c t R e p o s i t o r y , s e s s i o n ) => st
batches = repo . l i s t ()
i f not i s v a l i d s k u ( l i n e . sku , b a t c h e s ) :

Copyright © 2021, 2022 Lan Hui 148


r a i s e I n v a l i d S k u ( f ” I n v a l i d s k u { l i n e . s k u }” )
b a t c h r e f = model . a l l o c a t e ( l i n e , b a t c h e s )
s e s s i o n . commit ( )
return b a t c h r e f

Example: the allocate’s second argument (uow)’s type is


Abstract Base Class. We can pass a uow of FakeUnitOfWork,
or a uow of SqlAlchemyUnitOfWork.
# from s e r v i c e s . py i n Chapter 8
def a l l o c a t e (
o r d e r i d : str , sku : str , qty : int ,
uow : u n i t o f w o r k . A b s t r a c t U n i t O f W o r k ,
) => s t r :
l i n e = O r d e r L i n e ( o r d e r i d , sku , q t y )
w i t h uow :
p r o d u c t = uow . p r o d u c t s . g e t ( s k u= l i n e . s k u )

Copyright © 2021, 2022 Lan Hui 149


i f p r o d u c t i s None :
r a i s e I n v a l i d S k u ( f ” I n v a l i d s k u { l i n e . s k u }” )
batchref = product . a l l o c a t e ( l i n e )
uow . commit ( )
return b a t c h r e f

Allow different implementations.


See What is Dependency Injection with Java Code Example.
See Figure 4-1 and 4-2 in Architectural principles.

• Constructor injection – inject through constructor.

• Setter injection – inject through setter.

Copyright © 2021, 2022 Lan Hui 150

You might also like