Aggregate 6 - Functional style¶
This example shows how to define and use your own immutable aggregate base class with a more “functional” style than example 5.
Base classes¶
The DomainEvent class is defined as a “frozen” Python dataclass.
@dataclass(frozen=True)
class DomainEvent:
originator_id: UUID
originator_version: int
timestamp: datetime
The Aggregate base class in this example is also defined as a “frozen” Python
dataclass.
@dataclass(frozen=True)
class Aggregate:
id: UUID
version: int
created_on: datetime
modified_on: datetime
The Snapshot class in this example is also defined as a “frozen” Python
dataclass that extends DomainEvent.
@dataclass(frozen=True)
class Snapshot(DomainEvent):
state: dict[str, Any]
@classmethod
def take(cls, aggregate: Aggregate) -> Snapshot:
return Snapshot(
originator_id=aggregate.id,
originator_version=aggregate.version,
timestamp=datetime_now_with_tzinfo(),
state=aggregate.__dict__,
)
A generic aggregate_projector function is also defined, which takes
a mutator function and returns a function that can reconstruct an aggregate of a particular type from an iterable
of domain events.
def aggregate_projector(
mutator: MutatorFunction[TAggregate],
) -> Callable[[TAggregate | None, Iterable[DomainEvent]], TAggregate | None]:
def project_aggregate(
aggregate: TAggregate | None, events: Iterable[DomainEvent]
) -> TAggregate | None:
for event in events:
aggregate = mutator(event, aggregate)
return aggregate
return project_aggregate
Domain model¶
The Dog aggregate class is defined as immutable frozen data class
that extends the aggregate base class.
@dataclass(frozen=True)
class Dog(Aggregate):
name: str
tricks: tuple[str, ...]
The aggregate event classes, DogRegistered and
TrickAdded, are explicitly defined as separate module level classes.
@dataclass(frozen=True)
class DogRegistered(DomainEvent):
name: str
@dataclass(frozen=True)
class TrickAdded(DomainEvent):
trick: str
The aggregate commands, register_dog() and
add_trick() are defined as module level functions.
def register_dog(name: str) -> DomainEvent:
return DogRegistered(
originator_id=uuid4(),
originator_version=1,
timestamp=datetime_now_with_tzinfo(),
name=name,
)
def add_trick(dog: Dog, trick: str) -> DomainEvent:
return TrickAdded(
originator_id=dog.id,
originator_version=dog.version + 1,
timestamp=datetime_now_with_tzinfo(),
trick=trick,
)
The mutator function, mutate_dog(), is defined as a module level function.
@singledispatch
def mutate_dog(_: DomainEvent, __: Dog | None) -> Dog | None:
"""Mutates aggregate with event."""
@mutate_dog.register
def _(event: DogRegistered, _: None) -> Dog:
return Dog(
id=event.originator_id,
version=event.originator_version,
created_on=event.timestamp,
modified_on=event.timestamp,
name=event.name,
tricks=(),
)
@mutate_dog.register
def _(event: TrickAdded, dog: Dog) -> Dog:
return Dog(
id=dog.id,
version=event.originator_version,
created_on=dog.created_on,
modified_on=event.timestamp,
name=dog.name,
tricks=(*dog.tricks, event.trick),
)
@mutate_dog.register
def _(event: Snapshot, _: None) -> Dog:
return Dog(
id=event.state["id"],
version=event.state["version"],
created_on=event.state["created_on"],
modified_on=event.state["modified_on"],
name=event.state["name"],
tricks=tuple(event.state["tricks"]), # comes back from JSON as a list
)
The aggregate projector function, project_dog(), is defined as a module
level function by calling aggregate_projector() with
mutate_dog() as the argument.
project_dog = aggregate_projector(mutate_dog)
Application¶
The DogSchool application in this example uses the library’s
Application class. It must receive the new events that are returned
by the aggregate command methods, and pass them to its save()
method. The aggregate projector function must also be supplied when reconstructing an aggregate from the
repository, and when taking snapshots.
class DogSchool(Application[UUID]):
is_snapshotting_enabled = True
snapshot_class = Snapshot
def register_dog(self, name: str) -> UUID:
event = register_dog(name)
self.save(event)
return event.originator_id
def add_trick(self, dog_id: UUID, trick: str) -> None:
dog = self.repository.get(dog_id, projector_func=project_dog)
self.save(add_trick(dog, trick))
def get_dog(self, dog_id: UUID) -> dict[str, Any]:
dog = self.repository.get(dog_id, projector_func=project_dog)
return {"name": dog.name, "tricks": dog.tricks}
Test case¶
The TestDogSchool test case shows how the
DogSchool application can be used.
class TestDogSchool(TestCase):
def test_dog_school(self) -> None:
# Construct application object.
school = DogSchool()
# Evolve application state.
dog_id = school.register_dog("Fido")
school.add_trick(dog_id, "roll over")
school.add_trick(dog_id, "play dead")
# Query application state.
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead")
# Select notifications.
notifications = school.notification_log.select(start=1, limit=10)
assert len(notifications) == 3
# Take snapshot.
school.take_snapshot(dog_id, version=3, projector_func=project_dog)
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead")
# Continue with snapshotted aggregate.
school.add_trick(dog_id, "fetch ball")
dog = school.get_dog(dog_id)
assert dog["name"] == "Fido"
assert dog["tricks"] == ("roll over", "play dead", "fetch ball")
Code reference¶
- class examples.aggregate6.baseclasses.DomainEvent(originator_id: 'UUID', originator_version: 'int', timestamp: 'datetime')[source]¶
Bases:
object- originator_id: UUID¶
- originator_version: int¶
- timestamp: datetime¶
- class examples.aggregate6.baseclasses.Aggregate(id: 'UUID', version: 'int', created_on: 'datetime', modified_on: 'datetime')[source]¶
Bases:
object- id: UUID¶
- version: int¶
- created_on: datetime¶
- modified_on: datetime¶
- class examples.aggregate6.baseclasses.Snapshot(originator_id: 'UUID', originator_version: 'int', timestamp: 'datetime', state: 'dict[str, Any]')[source]¶
Bases:
DomainEvent- state: dict[str, Any]¶
- examples.aggregate6.baseclasses.aggregate_projector(mutator: MutatorFunction[TAggregate]) Callable[[TAggregate | None, Iterable[DomainEvent]], TAggregate | None][source]¶
- class examples.aggregate6.domainmodel.Dog(id: 'UUID', version: 'int', created_on: 'datetime', modified_on: 'datetime', name: 'str', tricks: 'tuple[str, ...]')[source]¶
Bases:
Aggregate- name: str¶
- tricks: tuple[str, ...]¶
- class examples.aggregate6.domainmodel.DogRegistered(originator_id: 'UUID', originator_version: 'int', timestamp: 'datetime', name: 'str')[source]¶
Bases:
DomainEvent- name: str¶
- class examples.aggregate6.domainmodel.TrickAdded(originator_id: 'UUID', originator_version: 'int', timestamp: 'datetime', trick: 'str')[source]¶
Bases:
DomainEvent- trick: str¶
- examples.aggregate6.domainmodel.register_dog(name: str) DomainEvent[source]¶
- examples.aggregate6.domainmodel.add_trick(dog: Dog, trick: str) DomainEvent[source]¶
- examples.aggregate6.domainmodel.mutate_dog(_: DomainEvent, __: Dog | None) Dog | None[source]¶
- examples.aggregate6.domainmodel.mutate_dog(event: DogRegistered, _: None) Dog
- examples.aggregate6.domainmodel.mutate_dog(event: TrickAdded, dog: Dog) Dog
- examples.aggregate6.domainmodel.mutate_dog(event: Snapshot, _: None) Dog
Mutates aggregate with event.
- class examples.aggregate6.application.DogSchool(env: Mapping[str, str] | None = None)[source]¶
Bases:
Application[UUID]- is_snapshotting_enabled: bool = True¶
- name = 'DogSchool'¶