def test_allocating_to_a_batch_reduces_the_available_quantity():
batch = Batch("batch-001", "SMALL-TABLE", qty=20, eta=date.today())
line = OrderLine('order-ref', "SMALL-TABLE", 2)
batch.allocate(line)
assert batch.available_quantity == 18
# immutable dataclass with no behavior
@dataclass(frozen=True)
class OrderLine:
orderid: str
sku: str
qty: int
class Batch:
def __init__(self, ref: str, sku: str, qty: int, eta: Optional[date]):
self.reference = ref
self.sku = sku
self.eta = eta
self.available_quantity = qty
def allocate(self, line: OrderLine):
self.available_quantity -= line.qty
+) 느낀점 : models.py 안에 다 넣어놓네..? Flask 를 쓰긴 했지만, 이게 맞는 건가? model 을 따로 만들어야 하나..?
from dataclasses import dataclass
from typing import NewType
Quantity = NewType("Quantity", int)
Sku = NewType("Sku", str)
Reference = NewType("Reference", str)
...
class Batch:
def __init__(self, ref: Reference, sku: Sku, qty: Quantity):
self.sku = sku
self.reference = ref
self._purchased_quantity = qty
Value Object
@dataclass(frozen=True)
class OrderLine:
orderid: OrderReference
sku: ProductReference
qty: Quantity
def test_name_equality():
assert Name("Harry", "Percival") != Name("Barry", "Percival")
class Person:
def __init__(self, name: Name):
self.name = name
+) explicit in code by implementing equality operators on entites
class Batch:
...
def __eq__(self, other): # magic method
if not isinstance(other, Batch):
return False
return other.reference == self.reference
def __hash__(self): # compare by this value
return hash(self.reference)
def test_prefers_current_stock_batches_to_shipments():
in_stock_batch = Batch("in-stock-batch", "RETRO-CLOCK", 100, eta=None)
line = OrderLine("oref", "RETRO-CLOCK", 10)
allocate(line, [in_stock_batch])
assert in_stock_batch.available_quantity == 90
def allocate(line: OrderLine, batches: List[Batch]) -> str:
batch = next(
b for b in sorted(batches) if b.can_allocate(line)
)
batch.allocate(line)
return batch.reference
class Batch:
...
def __gt__(self, other):
if self.eta is None:
return False
if other.eta is None:
return True
return self.eta > other.eta
class OutOfStock(Exception):
pass
def allocate(line: OrderLine, batches: List[Batch]) -> str:
try:
batch = next(
...
except StopIteration:
raise OutOfStock(f'Out of stock for sku {line.sku}')
from sqlalchemy.orm import mapper, relationship
import model
metadata = MetaData()
order_lines = Table(
'order_lines', metadata,
Column('id', Integer, primary_key=True, autoincrement=True),
Column('sku', String(255)),
Column('qty', Integer, nullable=False),
Column('orderid', String(255)),
)
...
def start_mappers():
# does its magic to bind our domain model classes to the barious tables we've defined
lines_mapper = mapper(model.OrderLine, order_lines)
def test_orderline_mapper_can_load_lines(session):
session.execute(
'INSERT INTO order_lines (orderid, sku, qty) VALUES ("order1", "RED-CHAIR", 12)'
)
expected = [
model.OrderLine("order1", "RED-CHAIR", 12)
]
assert session.query(model.OrderLine).all() == expected
import all_my_data
def create_a_batch():
batch = Batch(...)
all_my_data.batches.add(batch)
.save()
methodclass AbstractRepository(abc.ABC):
@abc.abstractmethod
def add(self, batch: model.Batch):
raise NotImplementedError
@abc.abstractmethod
def get(self, reference) -> model.Batch:
raise NotImplementedError
def test_repository_can_save_a_batch(session):
batch = model.Batch("batch1", "RUSTY-SOAPDISH", 100, eta=None)
repo = repository.SqlAlchemyRepository(session)
repo.add(batch)
session.commit()
rows = list(session.execute(
'SELECT reference, sku, _purchased_quantity, eta FROM "batches"'))
assert rows == [("batch1", "RUSTY-SOAPDISH", 100, None)]
class SqlAlchemyRepository(AbstractRepository): d
ef __init__(self, session):
self.session = session
def add(self, batch):
self.session.add(batch)
def get(self, reference):
return self.session.query(model.Batch).filter_by(reference=reference).one()
def list(self):
return self.session.query(model.Batch).all()
@flask.route.gubbins
def allocate_endpoint():
batches = SqlAlchemyRepository.list()
lines = [
OrderLine(l['orderid'], l['sku'], l['qty']) for l in request.params...
]
allocate(lines, batches)
session.commit()
return 201
+) Chapter 2. Repository Pattern Excercise
class FakeRepository(AbstractRepository):
def __init__(self, batches):
self._batches = set(batches)
def add(self, batch):
self._batches.add(batch)
def get(self, reference):
return next(b for b in self._batches if b.reference == reference)
def list(self):
return list(self._batches)
+) 진짜..? 흠..
What makes a good abstraction?
What do wwe want from abstractions?
coupled
+) 이렇게까지 말할 필요가 있어..? ㅠ 흑..
여기는 그냥 여러 Example 들이 있는 데 왜 난 보고 싶지 않지
domain service
& service layer
we will need to replace or delete these tests, because they are tightly coupled to a particular implementation
def allocate(
orderid: str, sku: str, qty: int,
uow: unit_of_work.AbstractUnitOfWork
) -> str:
line = OrderLine(orderid, sku, qty)
with uow:
batches = uow.batches.list()
...
batchref = model.allocate(line, batches)
uow.commit()
class SqlAlchemyUnitOfWork(AbstractUnitOfWork):
def __init__(self, session_factory=DEFAULT_SESSION_FACTORY):
self.session_factory = session_factory
...
class FakeUnitOfWork(unit_of_work.AbstractUnitOfWork):
def __init__(self):
self.batches = FakeRepository([])
...
FakeUnitOfWork
and FakeRepository
are tightly coupled, just like the real UnitOfWork
, Repository
classesdef test_add_batch():
uow = FakeUnitOfWork()
services.add_batch("b1", "CRUNCHY-ARMCHAIR", 100, None, uow)
assert uow.batches.get("b1") is not None
assert uow.committed
Don't mock what you don't own
- forces us to build these simple abstractions over messy subsystems
def get(self, sku):
return self.session.query(model.Product) \
.filter_by(sku=sku) \
.with_for_update() \
.first()