Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Event-Sourced Domain Models in Python

Event-Sourced Domain Models in Python

The introductory slides for a workshop on applying some classic patterns from Domain-Driven Design (DDD) in Python-based systems, which storage in a simple event store.

Robert Smallshire

September 20, 2015
Tweet

More Decks by Robert Smallshire

Other Decks in Programming

Transcript

  1. 2 Engaging training from seasoned professionals DDD Patterns in Python

    Is this the course for you? Domain-Driven Design (DDD) is an approach to software development that emphasizes high- fidelity modeling of the problem domain, and which uses a software implementation of the domain model as a foundation for system design. This approach helps organize and minimize the essential complexity of your software. Python is a highly productive, easy-to-learn, lightweight programming language that minimizes accidental complexity in the solution domain. This two day course will teach you the fundamentals of classical and modern DDD patterns in the context of Python. • We start by introducing the philosophy and principles of DDD. • We move quickly into hands-on domain modeling. • We implement a stand-alone domain model in pure Python code. • Taught on Windows, Linux or Mac OS X • Knowledge level of our Python for Programmers course is assumed Key Topics • Domain discovery • Bounded contexts and subdomains • Entity and value types • Aggregates • Domain events • Architecture • Persistence • Repositories • Projections • Domain services sixty-north.com
  2. 4

  3. 4

  4. 6

  5. a route to expressive and valuable software models Domain Driven

    Design is... ‣ a philosophy of bringing software closer to the problem domain • involving domain experts ‣ a systematic approach intertwining design and development • strategic design practices • fundamental principles • guidelines, rules and patterns 7
  6. 9

  7. 9

  8. 10 encapsulate with MODEL-DRIVEN DESIGN express model with isolate domain

    with encapsulate with ENTITIES VALUE OBJECTS LAYERED ARCHITECTURE AGGREGATES REPOSITORIES act as root of FACTORIES encapsulate with express model with encapsulate with access with encapsulate with access with DOMAIN EVENTS express model with SERVICES push state change with CONTEXT MAP overlap allied contexts through minimize translation support multiple clients through formalize as CONTINUOUS INTEGRATION CUSTOMER /SUPPLIER CONFORMIST OPEN HOST SERVICE SEPARATE WAYS PUBLISHED LANGUAGE SHARED KERNEL relate allied contexts as free teams to go ANTI- CORRUPTION LAYER translate and insulate unilaterally with BOUNDED CONTEXT keep model unified by assess/overview relationships with UBIQUITOUS LANGUAGE names enter BIG BALL OF MUD segregate the conceptual messes define model within model gives structure to CORE DOMAIN work in autonomous, clean GENERIC SUBDOMAINS avoid overinvesting in cultivate rich model with express model with by Eric Evans http://domainlanguage.com/ddd/patterns/ Eric Evans
  9. 12 ‣ Entities • identifiable • probably mutable • composite

    ‣ Values • measure/describe • equivalent • replaceable • self-contained • preferably immutable
  10. 12 entity value ‣ Entities • identifiable • probably mutable

    • composite ‣ Values • measure/describe • equivalent • replaceable • self-contained • preferably immutable
  11. 13 entity value ‣ Aggregates • consistency boundaries • transaction

    / unit-of- work modifies only one aggregate • not just object clusters • may only have one entity
  12. 13 entity value aggregate ‣ Aggregates • consistency boundaries •

    transaction / unit-of- work modifies only one aggregate • not just object clusters • may only have one entity
  13. 14 entity value aggregate ‣ Aggregate Root Entity • maintains

    aggregate consistency • hosts commands (methods) which modify the aggregate • target for inbound aggregate references • all inter-aggregate references are by root entity ID
  14. 14 entity value aggregate root ‣ Aggregate Root Entity •

    maintains aggregate consistency • hosts commands (methods) which modify the aggregate • target for inbound aggregate references • all inter-aggregate references are by root entity ID
  15. 15 entity value aggregate root ‣ Domain Events • Something

    which happens which domain experts care about • Significant state transitions • Publish-subscribe rather than subject-observer
  16. 15 entity value aggregate root event ‣ Domain Events •

    Something which happens which domain experts care about • Significant state transitions • Publish-subscribe rather than subject-observer
  17. 16 entity value aggregate root event ‣ Factories • Facilitate

    entity (and thereby aggregate) construction • Allow us to express the ubiquitous language - verbs rather than nouns (constructors) • Hide construction details (e.g. entity ID generation)
  18. 16 entity value aggregate root factory event ‣ Factories •

    Facilitate entity (and thereby aggregate) construction • Allow us to express the ubiquitous language - verbs rather than nouns (constructors) • Hide construction details (e.g. entity ID generation)
  19. 17 entity value aggregate root factory event ‣ Repositories •

    Store aggregates. • Usually one repo. per aggregate type. • Abstraction over persistence mechanism. • Architecturally significant!
  20. 17 entity value aggregate root factory repository event ‣ Repositories

    • Store aggregates. • Usually one repo. per aggregate type. • Abstraction over persistence mechanism. • Architecturally significant!
  21. 18 entity value aggregate root factory repository event ‣ Bounded

    Contexts • Scope of a ubiquitous language. • Segregated models • Independent, autonomous implementations • Align with technical components • Loosely coupled
  22. 18 entity value aggregate bounded context root factory repository event

    ‣ Bounded Contexts • Scope of a ubiquitous language. • Segregated models • Independent, autonomous implementations • Align with technical components • Loosely coupled
  23. 19 entity value aggregate root factory repository event bounded context

    ‣ Context maps • Integrations between bounded contexts • Logical mapping (concepts) • Physical mapping (actual software!) • Many integration patterns e.g. anti- corruption layer or open host service
  24. 19 entity value aggregate context map root factory repository event

    bounded context ‣ Context maps • Integrations between bounded contexts • Logical mapping (concepts) • Physical mapping (actual software!) • Many integration patterns e.g. anti- corruption layer or open host service
  25. 20 encapsulate with MODEL-DRIVEN DESIGN express model with isolate domain

    with encapsulate with ENTITIES VALUE OBJECTS LAYERED ARCHITECTURE AGGREGATES REPOSITORIES act as root of FACTORIES encapsulate with express model with encapsulate with access with encapsulate with access with DOMAIN EVENTS express model with SERVICES push state change with CONTEXT MAP overlap allied contexts through minimize translation support multiple clients through formalize as CONTINUOUS INTEGRATION CUSTOMER /SUPPLIER CONFORMIST OPEN HOST SERVICE SEPARATE WAYS PUBLISHED LANGUAGE SHARED KERNEL relate allied contexts as free teams to go ANTI- CORRUPTION LAYER translate and insulate unilaterally with BOUNDED CONTEXT keep model unified by assess/overview relationships with UBIQUITOUS LANGUAGE names enter BIG BALL OF MUD segregate the conceptual messes define model within model gives structure to CORE DOMAIN work in autonomous, clean GENERIC SUBDOMAINS avoid overinvesting in cultivate rich model with express model with by Eric Evans http://domainlanguage.com/ddd/patterns/ Eric Evans
  26. Foo Nothing we have seen requires: ‣ complicated frameworks ‣

    proprietary infrastructure ‣ avante-garde architectures ‣ "enterprise" anything 22
  27. 23 We dynamic language developers are still (largely) building models

    in terms of infrastructure and frameworks Eric Evans (2004) but the world has moved over the last decade
  28. 23 We dynamic language developers are still (largely) building models

    in terms of infrastructure and frameworks Eric Evans (2004) in
  29. 24 “The Object Relational Mapper takes two brilliant ideas and

    incapacitates them both.” Eric Evans, Domain Language Inc. at Skills Matter DDD eXchange 2013
  30. 26 http://jeffreypalermo.com/blog/ the-onion-architecture-part-1/ These architectures place the model at the

    centre and externalise infrastructure The Onion Architecture Jeffrey Palermo
  31. 31

  32. 32

  33. 33

  34. time “Current state is the left-fold over previous behaviours” Greg

    Young functools.reduce( ) events = ] [ append-only
  35. time “Current state is the left-fold over previous behaviours” Greg

    Young functools.reduce( ) events = ] [ apply_event, events, initial_model_state append-only
  36. time events = ] [ reconstitute domain model state at

    any time “projections” (time dependent queries) of the event stream
  37. 38 class Entity: """The base class of all entities. Attributes:

    id: A unique identifier. version: An integer version. discarded: True if this entity should no longer be used, otherwise False. """ def __init__(self, id, version): self._id = id self._version = version self._discarded = False def _increment_version(self): self._version += 1 @property def id(self): """A string unique identifier for the entity.""" self._check_not_discarded() return self._id @property def version(self): """An integer version for the entity.""" self._check_not_discarded() return self._version @property def discarded(self): """True if this entity is marked as discarded, otherwise False.""" return self._discarded def _check_not_discarded(self): if self._discarded: raise DiscardedEntityError("Attempt to use {}".format(repr(self))) class DiscardedEntityError(Exception): """Raised when an attempt is made to use a discarded Entity.""" pass pass id as argument (don't create a new id here) maintain a discarded flag consider maintaining a version
  38. 38 class Entity: """The base class of all entities. Attributes:

    id: A unique identifier. version: An integer version. discarded: True if this entity should no longer be used, otherwise False. """ def __init__(self, id, version): self._id = id self._version = version self._discarded = False def _increment_version(self): self._version += 1 @property def id(self): """A string unique identifier for the entity.""" self._check_not_discarded() return self._id @property def version(self): """An integer version for the entity.""" self._check_not_discarded() return self._version @property def discarded(self): """True if this entity is marked as discarded, otherwise False.""" return self._discarded def _check_not_discarded(self): if self._discarded: raise DiscardedEntityError("Attempt to use {}".format(repr(self))) class DiscardedEntityError(Exception): """Raised when an attempt is made to use a discarded Entity.""" pass pass id as argument (don't create a new id here) maintain a discarded flag consider maintaining a version
  39. 39 !"" application # !"" __init__.py # $"" __main__.py Application

    services. !"" bounded_context One package per bounded context # !"" __init__.py # $"" domain # !"" __init__.py # !"" model # # !"" __init__.py # # !"" aggregate_large Large complex aggregates should be their own package # # # !"" __init__.py This will expose the API of the aggregate at package level # # # !"" root_entity.py The root entity - responsible for consistency of the aggregate # # # !"" other_entity.py Another entity which forms part of this aggregate # # # !"" factories.py Factory functions for creating aggregates # # # $"" repository.py Aggregate persistence (Abstract if following hexagonal architecture) # # !"" aggregate_small.py Small, simple aggregates (most of them) can be a single file module # # $"" events.py Singleton publish-subscribe hub for domain events. Event base class. # $"" services # !"" __init__.py # !"" calculation_service.py Implement significant computations which involve multiple aggregates # $"" manipulation_service.py Perform significant manipulations involving for than one aggregate (care!) $"" infrastructure !"" __init__.py $"" repositories !"" __init__.py !"" large_aggregate_repo.py A concrete repository - in terms of the persistence infrastructure $"" small_aggregate_repo.py for each abstract repository type on the domain model Remember to use the ubiquitous language!
  40. 40 from bounded_context.domain.model.entity import Entity class User(Entity): def __init__(self, user_id,

    user_version, name): """DO NOT CALL DIRECTLY. """ super().__init__(user_id, user_version) self._name = name def __repr__(self): return "{d}User(id={id!r}, name={name!r})".format( d="*Discarded* " if self._discarded else "", id=self.id, name=self._name) @property def name(self): self._check_not_discarded() return self._name @name.setter def name(self, value): self._check_not_discarded() if len(value) < 1: raise ValueError("User name cannot be empty") self._name =value self._increment_version() def register_customer(name): user = User(user_id=uuid.uuid4().hex, user_version=0, name=name) return work_item factory creates id ubiquitous language mutators increment version queries and mutators check liveness __repr__() makes song and dance about liveness
  41. 40 from bounded_context.domain.model.entity import Entity class User(Entity): def __init__(self, user_id,

    user_version, name): """DO NOT CALL DIRECTLY. """ super().__init__(user_id, user_version) self._name = name def __repr__(self): return "{d}User(id={id!r}, name={name!r})".format( d="*Discarded* " if self._discarded else "", id=self.id, name=self._name) @property def name(self): self._check_not_discarded() return self._name @name.setter def name(self, value): self._check_not_discarded() if len(value) < 1: raise ValueError("User name cannot be empty") self._name =value self._increment_version() def register_customer(name): user = User(user_id=uuid.uuid4().hex, user_version=0, name=name) return work_item factory creates id ubiquitous language mutators increment version queries and mutators check liveness __repr__() makes song and dance about liveness
  42. 41 ‣ Lean heavily on built-in immutable types tuple, frozenset,

    str, etc ‣ Consider preventing attribute assignment by overriding __setattr__() ‣ Implement value equality/ inequality __eq__() and __ne__() ‣ Hashing by overriding __hash__() ‣ Side-effect free functions to produce new versions class Email: """An e-mail address """ @classmethod def from_text(cls, address): if '@' not in address: raise ValueError("Email address must contain '@'") local_part, _, domain_part = address.partition('@') return cls(local_part, domain_part) def __init__(self, local_part, domain_part): if len(local_part) + len(domain_part) > 255: raise ValueError("Email addresses too long") self._parts = (local_part, domain_part) def __str__(self): return '@'.join(self._parts) def __repr__(self): return "Email(local_part={!r}, domain_part={!r})".format(*self._parts) def __eq__(self, rhs): if not isinstance(rhs, Email): return NotImplemented return self._parts == rhs._parts def __ne__(self, rhs): return not (self == rhs) def __hash__(self): return hash(self._parts) @property def local(self): return self._parts[0] @property def domain(self): return self._parts[1] def replace(self, local=None, domain=None): return Email(local_part=local or self._parts[0], domain_part=domain or self._parts[1])
  43. 42 _event_handlers = {} def subscribe(event_predicate, subscriber): """Subscribe to events.

    Args: event_predicate: A callable predicate which is used to identify the events to which to subscribe. subscriber: A unary callable function which handles the passed event. """ if event_predicate not in _event_handlers: _event_handlers[event_predicate] = set() _event_handlers[event_predicate].add(subscriber) def unsubscribe(event_predicate, subscriber): """Unsubscribe from events. Args: event_predicate: The callable predicate which was used to identify the events to which to subscribe. subscriber: The subscriber to disconnect. """ if event_predicate in _event_handlers: _event_handlers[event_predicate].discard(subscriber) def publish(event): """Send an event to all subscribers. Each subscriber will receive each event only once, even if it has been subscribed multiple times, possibly with different predicates. Args: event: The object to be tested against by all registered predicate functions and sent to all matching subscribers. """ matching_handlers = set() for event_predicate, handlers in _event_handlers.items(): if event_predicate(event): matching_handlers.update(handlers) for handler in matching_handlers: handler(event)
  44. 43 class DomainEvent: """A base class for all events in

    this domain. DomainEvents are value objects and all attributes are specified as keyword arguments at construction time. There is always a timestamp attribute which gives the event creation time in UTC, unless specified. Events are equality comparable. """ def __init__(self, timestamp=_now, **kwargs): self.__dict__['timestamp'] = utc_now() if timestamp is _now else timestamp self.__dict__.update(kwargs) def __setattr__(self, key, value): raise AttributeError("DomainEvent attributes are read-only") def __eq__(self, rhs): return self.__dict__ == rhs.__dict__ def __ne__(self, rhs): return self.__dict__ != rhs.__dict__ def __hash__(self): return hash(tuple(self.__dict__.items())) def __repr__(self): return self.__class__.__qualname__ + "(" + ', '.join( "{0}={1!r}".format(*item) for item in self.__dict__.items()) + ')'
  45. 44 class Repository: __metaclass__ = ABCMeta def __init__(self, **kwargs): super().__init__(**kwargs)

    def all_users(self, user_ids=None): return self.users_where(lambda user: True, user_ids) def users_with_name(self, name, user_ids=None): return self.users_where(lambda user: user.name == name, user_ids) def users_with_id(self, user_id): try: return exactly_one(self.all_users((user_id,))) except ValueError as e: raise ValueError("No WorkItem with id {}".format(user_id)) from e @abstractmethod def users_where(self, predicate, user_ids=None): raise NotImplementedError convenience queries for retrieving users implemented in terms of a generic query Repository will be subclassed with an infrastructure-specific implementation, which can specialise all queries. subclass implementation must override at least this
  46. 44 class Repository: __metaclass__ = ABCMeta def __init__(self, **kwargs): super().__init__(**kwargs)

    def all_users(self, user_ids=None): return self.users_where(lambda user: True, user_ids) def users_with_name(self, name, user_ids=None): return self.users_where(lambda user: user.name == name, user_ids) def users_with_id(self, user_id): try: return exactly_one(self.all_users((user_id,))) except ValueError as e: raise ValueError("No WorkItem with id {}".format(user_id)) from e @abstractmethod def users_where(self, predicate, user_ids=None): raise NotImplementedError convenience queries for retrieving users implemented in terms of a generic query Repository will be subclassed with an infrastructure-specific implementation, which can specialise all queries. subclass implementation must override at least this
  47. 46

  48. 47

  49. To do Doing Done Work Item A Work Item B

    Work Item C Work Item D Work Item E register schedule advance advance retire abandon
  50. 49