=================================== Tutorial - Part 1 - Getting started =================================== This part of the :doc:`tutorial ` introduces the library's :class:`~eventsourcing.domain.Aggregate` and :class:`~eventsourcing.application.Application` classes, showing and explaining how they can be used together to write an event-sourced application in Python. Python classes ============== This tutorial depends on a basic understanding of `Object-Oriented Programming in Python `_. Before we begin, let's review object classes in Python. The example below is taken from the `Class and Instance Variables `_ section of the Python docs. The example below defines a ``Dog`` class. An instance of the ``Dog`` class can be constructed by calling the ``Dog`` class with a ``name`` argument. An instance of ``Dog`` will have a ``name`` attribute and a ``tricks`` attribute. The ``name`` attribute will be initialized with the given value of the ``name`` argument. The ``tricks`` attribute will be initialized with an empty list. The ``Dog`` class defines a method called ``add_trick()``. Calling the ``add_trick()`` method on an instance of ``Dog`` will cause the given value of the argument ``trick`` to be appended to its ``tricks`` attribute. .. code-block:: python class Dog: def __init__(self, name): self.name = name self.tricks = [] def add_trick(self, trick): self.tricks.append(trick) Let's construct an instance of the ``Dog`` class, by calling the ``Dog`` class with a ``name`` argument. .. code-block:: python dog = Dog(name='Fido') assert isinstance(dog, Dog) The ``__init__()`` method of the ``Dog`` class initialises the instance's ``name`` attribute with the given value of the ``name`` argument, and the ``tricks`` attribute to be an empty list. .. code-block:: python assert dog.name == 'Fido' assert dog.tricks == [] Let's call the method ``add_trick()``. The given value of the ``trick`` argument will be appended to the object's ``tricks`` attribute. .. code-block:: python dog.add_trick(trick='roll over') assert dog.tricks == ['roll over'] This is a simple example of a Python class. In the next section, we will convert the ``Dog`` class to be an event-sourced aggregate. Event-sourced aggregates ======================== In *Domain-Driven Design*, aggregates are persistent software objects that may change. Aggregates are persisted by somehow recording their state in a database. When the state of an aggregate changes, the recorded state in the database is somehow updated. A conventional way to persist the state of aggregates in *Domain-Driven Design* is to have a database table for each type of aggregate in the domain model, with one column for each attribute, and with one row for each aggregate. When a new aggregate is created, a row is inserted. When an existing aggregate is changed, its row is updated. This approach is known as "concrete table inheritance". Event sourcing takes this two steps further. Firstly, whenever an aggregate is created or updated, a decision is encapsulated by an event object, and that event object is used to mutate an aggregate object. Secondly, rather than recording only the current state of aggregate objects, instead the event objects are recorded so that the recorded event objects can be used later to reconstruct aggregate objects. All events can be recorded in the same table, with one row inserted for each new event. The event objects do not change, and so the rows are not updated. The important difference for an event-sourced aggregate is that it will generate a sequence of event objects, and this sequence of event objects will be used to reconstruct the aggregate object. The software code that can persist and reconstruct event-sourced aggregates can be used in many different applications. We can most concisely define event-sourced aggregates in Python by using the :class:`~eventsourcing.domain.Aggregate` class and the :func:`@event` decorator. .. code-block:: python from eventsourcing.domain import Aggregate, event For example, we can convert the ``Dog`` class into an event-sourced aggregate, by inheriting from the library's :class:`~eventsourcing.domain.Aggregate` class, and by decorating "command" methods (methods that change the state of the aggregate) with the library's :func:`@event` decorator. By decorating command methods in this way, event object classes will be defined according to the method signatures, new event objects will be constructed whenever the methods are called, and the method bodies will be executed whenever the event objects are used to mutate aggregate objects. The changes are highlighted below. .. #include-when-testing .. from eventsourcing.utils import clear_topic_cache clear_topic_cache() del Dog .. code-block:: python :emphasize-lines: 1,2,7 class Dog(Aggregate): @event('Registered') def __init__(self, name): self.name = name self.tricks = [] @event('TrickAdded') def add_trick(self, trick): self.tricks.append(trick) As above in the simple example of a Python class, calling this ``Dog`` class will construct a new instance. Behind the scenes, a ``Dog.Registered`` event is also constructed, but we will return to this later. .. code-block:: python dog = Dog(name='Fido') The variable ``dog`` is an instance of ``Dog``. .. code-block:: python assert isinstance(dog, Dog) The ``dog`` object is also an :class:`~eventsourcing.domain.Aggregate`. .. code-block:: python assert isinstance(dog, Aggregate) As above, the attributes ``name`` and ``tricks`` have been initialised. .. code-block:: python assert dog.name == 'Fido' assert dog.tricks == [] Because ``Dog`` inherits from :class:`~eventsourcing.domain.Aggregate`, the ``dog`` object also has an ``id`` attribute. It happens to be a version 4 UUID. The aggregate's ``id`` can be used to uniquely identify the aggregate in a domain model. .. code-block:: python from uuid import UUID assert isinstance(dog.id, UUID) When we call the method ``add_trick()``, the given value of the ``trick`` argument is appended to the ``tricks`` attribute. Behind the scenes a ``Dog.TrickAdded`` event is also constructed. .. code-block:: python dog.add_trick(trick='roll over') assert dog.tricks == ['roll over'] The ``Dog`` class is an event-sourced aggregate. An event object was constructed both when we called the ``Dog`` class, and when we called the ``add_trick()`` method. These two event objects can be collected from the aggregate object, and recorded, and used later to reconstruct the aggregate. We can collect newly constructed event objects from the aggregate object by calling the :func:`~eventsourcing.domain.Aggregate.collect_events` method, which is defined by the :class:`~eventsourcing.domain.Aggregate` class. .. code-block:: python events = dog.collect_events() The variable ``events`` is a list of event objects. The event objects in this list are all instances of the nested class ``Dog.Event``. .. code-block:: python for e in events: assert isinstance(e, Dog.Event) Two event objects were collected. .. code-block:: python assert len(events) == 2 The first event object is a ``Dog.Registered`` event. It has an attribute ``name``. .. code-block:: python assert isinstance(events[0], Dog.Registered) assert events[0].name == 'Fido' The second event object is a ``Dog.TrickAdded`` event. It has an attribute ``trick``. .. code-block:: python assert isinstance(events[1], Dog.TrickAdded) assert events[1].trick == 'roll over' Each event object has a :func:`~eventsourcing.domain.CanMutateAggregate.mutate` method. We can reconstruct the aggregate object, by calling the :func:`~eventsourcing.domain.CanMutateAggregate.mutate` methods, in the following way. .. code-block:: python copy = None for e in events: copy = e.mutate(copy) assert copy == dog This technique for reconstructing aggregates from events is used by the application repository in the next section. Event-sourced aggregates can be developed and tested independently of each other, and independently of any persistence infrastructure. If you are feeling playful, type the Python code in this example into a Python console and see for yourself that it works. Use a debugger to step through the code. Use a testing framework or module to express this code as a unit test. Event-sourced aggregates are normally used within an application object, so that aggregate events can be recorded in a database, and so that aggregates can be reconstructed from recorded events. Event-sourced applications ========================== Event-sourced applications combine event-sourced aggregates with a persistence mechanism to store and retrieve aggregate events. Event-sourced applications define "command" methods and "query" methods that can be used by interfaces to get and update the state of an application without dealing directly with its aggregates. We can most easily define event-sourced applications by using the :class:`~eventsourcing.application.Application` class. .. code-block:: python from eventsourcing.application import Application Using the :class:`~eventsourcing.application.Application` class, and the ``Dog`` class, let's define a ``DogSchool`` application. .. code-block:: python class DogSchool(Application): def register_dog(self, name): dog = Dog(name) self.save(dog) return dog.id def add_trick(self, dog_id, trick): dog = self.repository.get(dog_id) dog.add_trick(trick=trick) self.save(dog) def get_dog(self, dog_id): dog = self.repository.get(dog_id) return {'name': dog.name, 'tricks': tuple(dog.tricks)} The command methods ``register_dog()`` and ``add_trick()`` use the application's :func:`~eventsourcing.application.Application.save` method to collect and store new event objects. The command method ``add_trick()`` and the query method ``get_dog()`` reconstruct aggregates from stored events by using the :func:`~eventsourcing.application.Repository.get` method of the application's repository object. We can use the ``DogSchool`` class to construct an application object. .. code-block:: python application = DogSchool() We can create and update aggregates by calling ``register_dog()`` and ``add_trick()``. .. code-block:: python dog_id = application.register_dog(name='Fido') application.add_trick(dog_id, trick='roll over') application.add_trick(dog_id, trick='fetch ball') We can get the state of an aggregate by calling ``get_dog()``. .. code-block:: python dog_details = application.get_dog(dog_id) assert dog_details['name'] == 'Fido' assert dog_details['tricks'] == ('roll over', 'fetch ball') We can propagate the state of an application by selecting event notifications from the application's notification log. The notification log presents the events of all aggregates in an application as a single sequence of event notifications. .. code-block:: python notifications = application.notification_log.select(start=1, limit=10) assert len(notifications) == 3 assert notifications[0].id == 1 assert notifications[1].id == 2 assert notifications[2].id == 3 There will be exactly one event notification for each aggregate event that was stored. The event notifications will be in the same order as the aggregate events were stored. The events of all aggregates will appear in the notification log. Please note, when we interacted with the application methods, we did not directly interact with the aggregates. The aggregates are encapsulated by the application. In this way, event-sourced applications can be developed and tested independently. The :class:`~eventsourcing.application.Application` class, by default, uses a persistence module which stores events in memory using "plain old Python objects". Application objects can be configured with environment variables to use a durable database. If you are feeling playful, please type the Python code into a Python console and see for yourself that it works. Writing tests ============= It is generally recommended to follow a test-driven approach to the development of event-sourced applications. You can get started by first writing a failing test for your application in a Python module, for example with the following test in a file ``test_application.py``. .. code-block:: python def test_dog_school(): # Construct the application. app = DogSchool() # Register a dog. dog_id = app.register_dog(name='Fido') # Check the dog has been registered. assert app.get_dog(dog_id) == { 'name': 'Fido', 'tricks': (), } # Add tricks. app.add_trick(dog_id, trick='roll over') app.add_trick(dog_id, trick='fetch ball') # Check the tricks have been added. assert app.get_dog(dog_id) == { 'name': 'Fido', 'tricks': ('roll over', 'fetch ball'), } You can begin to develop your application by defining your application and aggregate classes in the test module. You can then refactor by moving your application and aggregate classes to separate modules. For example your application class could be moved to an ``application.py`` file, and your aggregate classes could be moved to a ``domainmodel.py`` file. See the "live coding" video :ref:`Event sourcing in 15 minutes ` for a demonstration of how this can be done. Project structure ================= You are free to structure your project files however you wish. It is generally recommended to put test code and code-under-test in separate folders. :: your_project/__init__.py your_project/application.py your_project/domainmodel.py tests/__init__.py tests/test_application.py If you will have a larger number of aggregate classes, you may wish to convert the ``domainmodel.py`` file into a Python package, and have a separate submodule for each aggregate class. To start a new project with modern tooling, you can use the `template for Python eventsourcing projects `_. Exercise ======== Please note, completing the exercises in this tutorial depends on: * having a working Python installation, * :doc:`installing the eventsourcing library `, and * knowing how to `write and run tests in Python `_. 1. Copy the ``test_dog_school()`` function (see above) into a Python file, for example ``test_application.py``. Then run the test function and see that it fails. Then add the ``DogSchool`` application and the ``Dog`` aggregate code. Then run the test function again and see that it passes. .. code-block:: python test_dog_school() 2. When your code is working, refactor by moving the application and aggregate classes to separate Python files, for example ``application.py`` and ``domainmodel.py``. After completing your refactorings, run the test again to make sure your code still works. 3. If you are feeling playful, you can use a debugger or add some print statements to step through what happens in the aggregate and application classes. Next steps ========== * To continue this tutorial, please read :doc:`Part 2 `. * See also the :doc:`/topics/examples`.