Broken Rules Pattern with Epiphany

broken rulesThe Broken Rules pattern* provides an encapsulated way for objects to indicate whether or not their internal state is valid. Here, “valid” usually means that the data contained in the object is ready to be persisted to a database. An object following this pattern will have two properties: isvalid and brokenrules. isvalid returns True or False to indicate whether or not the object has any broken rules. brokenrules will return a collection of validation problems if there are any.

Broken Rules for Entity Classes

To demonstrate, let’s create some entity classes.


class deities(entities):
    pass

class deity(entity):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

    def __str__(self):
        return self.name

pantheon = deities()
zeus = deity('Zeus', 'male')

assert pantheon.isvalid
assert pantheon.brokenrules.count == 0

assert zeus.isvalid
assert zeus.brokenrules.count == 0

Here, we define an entities collection class called deities and an entity class called deity. We instantiate one of each and we can immediately assert that each has an isvalid proprety that is True and a brokenrules collection property which is empty. These properties are available as a consequence of the classes inheriting from entities and entity. Both asserts mean the same thing since by definition, an entity with no broken rules is valid.

Now let’s redefine the deity class to contain validation logic so we can ensure it knows when it is invalid.


class deity(entity):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

    
    @property
    def brokenrules(self):
        brs = brokenrules()
        if self.gender not in ('female', 'male'):
            brs += '{} is an invalid gender.'.format(self.gender)

        if type(self.name) != str:
            brs += 'Deity names must be strings.'

        return brs
    
    def __str__(self):
        return self.name

zeus = deity(42, None)

assert not zeus.isvalid

assert str(zeus.brokenrules) == 'None is an invalid gender.\nDeity names must be strings.'

The first step is to override the brokenrules property. It creates and return its own brokenrules collection class each time it is called. We establish two validation rules: the first is that a deity must have a gender that is either “female” or “male”, and the second is that the deity’s name must be a string. Later, we instantiate a deity called zeus and deliberately brake both of these rules upon instantiation by making its name 42 and giving it a gender of None. Now, we can assert that zeus is not valid. We can also convert the brokenrules property to a string to get a \n delimited list of human readable broken rules.

Persistence

Epiphany entities don’t currently perform persistence routines (this will likely change in the future), but if we wanted to ensure that our invalid zeus is never saved to the database, we could write our save (INSERT/UPDATE) method like so.


class deity(entity):
    ⋮ 
    save(self):
        if self.isvalid:
            # INSERT/UPDATE state date into database
            pass
         else:
              raise Exception("Can't save to database. Object is in an invalid state.")
    ⋮ 

Here, isvalid is tested to ensure that invalid data is never persisted. However, if this were for a typical data entry application, the UI code should first test if the object is valid, and if is not, inform the user so the data can be corrected.

Broken Rules for Collections

Let’s see what happens when we add our invalid zeus to a deities collection.


pantheon = deities()
pantheon += zeus

assert not pantheon.isvalid
assert str(pantheon.brokenrules) == 'None is an invalid gender.\nDeity names must be strings.'

If an entities collection object contains one or more invalid entity objects, it will become invalid. Thus, having added zeus to the pantheon collection, we have now made pantheon invalid. Converting the entities collection’s brokenrules property to a string will list each of the broken rules in each of the invalid entity objects within the collection.

Recursive Broken Rules

When an entity has a collection of child entities, it is easy to create a validation rule that states that the entity is only valid if all of it’s child objects are valid.

Let’s say that our deity object has a collection called children(appropriately enough):


class deity(entity):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
        self.children = deities()

    @property
    def brokenrules(self):
        brs = brokenrules()
        if self.gender not in ('female', 'male'):
            brs += '{} is an invalid gender for "{}".'.format(self.gender, self.name)

        if type(self.name) != str:
            brs += 'Deity names must be strings.'

        brs += self.children.brokenrules

        return brs

    def __str__(self):
        return self.name


zeus = deity('Zeus', 'male')
zeus.children += deity('Aeacus', 'm')
zeus.children += deity('Angelos', 'female')
zeus.children += deity('Aphrodite', 'female')
zeus.children += deity('Apollo', 'male')
zeus.children += deity('Ares', 'male')
zeus.children += deity('Artemis', 'f')
zeus.children += deity('Athena', 'female')


assert not zeus.isvalid
assert str(zeus.brokenrules) == 'm is an invalid gender for "Aeacus".\nf is an invalid gender for "Artemis".'

Here we’ve redefined the deity entity to have a children property to contain all of the deity’s children. (Note that it’s a coincidence that this property is called children. Obviously, an object can (and usually would) have child collections for things other then literal children. deity and children have a one-to-many relationship with each other and that is the concept that is being illustrated here.)

We’ve also added a line in the brokenrules property to collect all the broken rules of the deity’s children objects. We’ve additionally made a minor adjustment to the broken rule text to indicate which deity the broken rule pertains to.

Now we use the new class by re-instantiating zeus and adding his children to his children collection. (We will stick to those of his children whose name started with “A” for brevity.)

Note that for Aeacus and Artemis, we’ve abbreviated the gender. This breaks a rule because gender must be either of the exact strings ‘male’ or ‘female’. Thus, zeus becomes invalid – not because the zeus itself is invalid, but because two of his children are in an invalid state. The brokenrule collection tells us which deity objects are invalid.

Conclusion

The Broken Rule pattern is a powerful way to centralize the validation rules of the object, and ensure that bad data is never persisted to the data store. Additionally, it is easy to capture any broken rules in an object graph by adding a single line of code in the brokenrule property.

Ideally, all validation could could happen in this property since it’s centralized and has the full power and expressiveness of the Python language to test values for validity. However, for reasons of optimization, it will still be necessary to duplicate many of these rules on the browser using JavaScript to improve the user’s experience. Also, some validation rules are so data-intensive that they should be implemented in the database itself (e.g., using check constraints, unique constraints, etc.). Regardless, I’ve found this pattern so useful that I use it for virtually all of the code I author.

* The Broken Rules pattern was likely first described in Rocky Lhotka’s book Professional Visual Basic 5.0 Business Objects published in 1997

Leave a Reply

Your email address will not be published. Required fields are marked *