Sutoppu (ストップ - Japanese from English Stop) is a simple python implementation of Specification pattern.
See Wikipedia.
In computer programming, the specification pattern is a particular software design pattern, whereby business rules can be recombined by chaining the business rules together using boolean logic. The pattern is frequently used in the context of domain-driven design.
More information: Eric Evans and Martin Fowler article about Specifications
$ pip install sutoppu
from sutoppu import Specification
class Fruit:
def __init__(self, color: str, sweet: bool, bitter: bool) -> None:
self.color = color
self.sweet = sweet
self.bitter = bitter
class FruitIsBitter(Specification[Fruit]):
description = 'The given fruit must be bitter.'
def is_satisfied_by(self, fruit: Fruit) -> bool:
return fruit.bitter is True
class FruitIsSweet(Specification[Fruit]):
description = 'The given fruit must be sweet.'
def is_satisfied_by(self, fruit: Fruit) -> bool:
return fruit.sweet is True
class FruitIsYellow(Specification[Fruit]):
description = 'The given fruit must be yellow.'
def is_satisfied_by(self, fruit: Fruit) -> bool:
return fruit.color == 'yellow'
>>> lemon = Fruit(color='yellow', sweet=False, bitter=True)
>>> is_a_lemon = FruitIsYellow() & FruitIsBitter() & ~FruitIsSweet()
>>> is_a_lemon.is_satisfied_by(lemon)
True
Sutoppu uses bitwise operator overloading to provide simplified syntax.
And:
>>> my_spec = SpecificationA() & SpecificationB()
Or:
>>> my_spec = SpecificationA() | SpecificationB()
Not:
>>> my_spec = ~SpecificationA()
If you find the is_satisfied_by
method inconvenient, you can alternatively call the specification as shown below.
>>> lemon = Fruit(color='yellow', sweet=False, bitter=True)
>>> is_a_lime = FruitIsGreen() & FruitIsBitter() & ~FruitIsSweet()
>>> is_a_lime(lemon)
False
It can be difficult to know which specification failed in complex concatenated rules. Sutoppu allows listing all the failed specifications by getting the errors
attribute after use.
The errors
attribute is reset each time the specification is used. For each failed specification, it returns a dict with the name of the specification class as key and the description provided in the class as value. In the case where the specification failed with a not
condition, the description is prefixed with Not ~
.
>>> apple = Fruit(color='red', sweet=True, bitter=False)
>>> is_a_lemon = FruitIsYellow() & FruitIsBitter() & ~ FruitIsSweet()
>>> is_a_lemon.is_satisfied_by(apple)
False
>>> is_a_lemon.errors
{
'FruitIsColored': 'The given fruit must be yellow.',
'FruitIsBitter': 'The given fruit must be bitter.',
'FruitIsSweet': 'Not ~ The given fruit must be sweet.'
}