HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Event sourcing with Python

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
withsourcingpythonevent

Problem

I've been studying the event sourcing pattern and trying to come up with a set of classes or minimal library I can share across projects.

Below you can find the implementation of this main parts of this pattern including the EventStore and Repository. I'd be thankful on getting a review on whether this is what the pattern is about and advice on where I could improve.

Note that I excluded classes Immutable, Identifiable and Aggregate for brevity but I hope their nouns are self-describing enough to provide the necessary context.

```
# -- coding: utf-8 --

from abc import ABCMeta, abstractmethod
from datetime import *
from . import Immutable, Identifiable, Aggregate, Repository, snake_case, \
ConventionBasedMessageHandler, ConventionBasedMessageBus, Serializer

class EventSource(Identifiable):

def __init__(self):
Identifiable.__init__(self)
self.version = 0
self._recorded_events = []

def get_version(self):
return self.version

def get_recorded_events(self):
return self._recorded_events

def clear_recorded_events(self):
self._recorded_events = []

def _record(self, event):
self._apply(event)
self._recorded_events.append(event)

_record_that = _record_event = _record # sugar alias

def _apply(self, event):
method_name = '_apply_' + snake_case(event.__class__.__name__)
method = getattr(self, method_name, None)
if not method or not callable(method):
raise Exception('no %s method defined in class' % (method_name))
method(event)

def load_from_history(self, events):
"""reconstitutes state of this object based on its history of events"""
for event in events:
self._apply(event)
self.version = len(events)

reconstitute_from_history = load_from_history # sugar alias

class AggregateWithEventSourcing(Aggregate, EventSource):

def __init__(self):
Aggregate.__init__(self)

Solution

I started to review your code. Look # review: comments bellow:

# Is `Identifiable` base class useful? Does it add something signifiant?
class EventSource(object):
    def __init__(self):
        # review: use `super` call here: that way you can change your base class
        super(EventSource, self).__init__()
        # review: add the `self.id` field here.
        self.id = None
        self.version = 0
        self._recorded_events = []

    # review: `get_version()` is useless since `self.version` is public
    # def get_version(self):
    #     return self.version

    def get_recorded_events(self):
        return self._recorded_events

    def clear_recorded_events(self):
        # review: if you want to "clear" a list, do it as follow.
        # That way, any reference to `self._recorded_events` won't be broken.
        self._recorded_events[:] = []

    def _record(self, event):
        self._apply(event)
        self._recorded_events.append(event)

    # review: No need to add "sugar alias" for "protected" methods.
    # _record_that = _record_event = _record  # sugar alias

    # review: It's more natural to raise `AttributeError` instead of `Exception`.
    def _apply(self, event):
        method_name = '_apply_' + snake_case(event.__class__.__name__)
        method = getattr(self, method_name)  # review: raise `AttributeError` if missing.
        method(event)  # review: raise `TypeError` if not callable

    def load_from_history(self, events):
        """reconstitutes state of this object based on its history of events"""
        for event in events:
            self._apply(event)
        self.version = len(events)

    # review: Is it really usefull
    reconstitute_from_history = load_from_history  # sugar alias


Additional remaks:

  • get_recorded_events() method returns self._recorded_events, which is a mutable object (a Python list), So, the end-user can modify it. If it can, drop this method and turn self._recorded_events to a "public" attribute: self.recorded_events.



  • self.id is not used in this class.



  • What is the role of self.version?



TL;TR.

In summary:

  • Try to be more Pythonic,



  • ABC/metaclasses are not useful,



  • There are too many one-line methods: consider refactoring,



  • The class hierarchy is unnecessary too complex,



  • You can use functions (really).



I think you design your application to be object oriented (probably from UML or Java). It's good, but in Python you can also use functional programing.

Code Snippets

# Is `Identifiable` base class useful? Does it add something signifiant?
class EventSource(object):
    def __init__(self):
        # review: use `super` call here: that way you can change your base class
        super(EventSource, self).__init__()
        # review: add the `self.id` field here.
        self.id = None
        self.version = 0
        self._recorded_events = []

    # review: `get_version()` is useless since `self.version` is public
    # def get_version(self):
    #     return self.version

    def get_recorded_events(self):
        return self._recorded_events

    def clear_recorded_events(self):
        # review: if you want to "clear" a list, do it as follow.
        # That way, any reference to `self._recorded_events` won't be broken.
        self._recorded_events[:] = []

    def _record(self, event):
        self._apply(event)
        self._recorded_events.append(event)

    # review: No need to add "sugar alias" for "protected" methods.
    # _record_that = _record_event = _record  # sugar alias

    # review: It's more natural to raise `AttributeError` instead of `Exception`.
    def _apply(self, event):
        method_name = '_apply_' + snake_case(event.__class__.__name__)
        method = getattr(self, method_name)  # review: raise `AttributeError` if missing.
        method(event)  # review: raise `TypeError` if not callable

    def load_from_history(self, events):
        """reconstitutes state of this object based on its history of events"""
        for event in events:
            self._apply(event)
        self.version = len(events)

    # review: Is it really usefull
    reconstitute_from_history = load_from_history  # sugar alias

Context

StackExchange Code Review Q#147033, answer score: 3

Revisions (0)

No revisions yet.