Specification pattern in Python

The specification pattern is a very powerful, elegant technique to encapsulate business rules as part of Domain Driven Design (DDD). Yet, this pattern might be among one of the most undervalued — or even unknown — design patterns.

Photo by Makarios Tang on Unsplash

This article describes what the specification pattern is, how it can be used and the benefits it has. Furthermore, we’ll provide example code in Python and a link to an open source project that can be used directly within your own project(s).

What is the specification pattern?

As mentioned before, the specification pattern is a technique to encapsulate business rules. Business rules can usually be written in a form that results in a yes/no answer; a boolean result.

A business rule might be: on this road, the maximum speed is 25 km/h.

The road in this case is a domain object and the maximum speed is 25 km/h is a rule or predicate, i.e., a filter criteria.

In software development you would have a class called Road and that class has a function or property called maximum_speed which would return 25.

If you have a list of Road objects, with various speed limits, and you want to know which roads have a maximum speed of 25 km/h, you can filter the list by the predicate: maximum_speed == 25. Now this predicate can be turned into a specification when we encapsulate it. We’ll show this later in this article.

The predicate above applies to Road objects, as Road objects provide a property called maximum_speed.

In a lot of software, predicates are hard-coded almost everywhere throughout the codebase. There are a few reasons for that:

  1. The predicate is tightly bound to the semantics of the surrounding function;
  2. The framework being used is advocating the use of hard-coded predicates, like most ORMs;
  3. Ignorance; this is the way we always do it. Are there even alternatives?

A common way of using predicates

Let’s break down a code example:

@dataclass
class Road:
maximum_speed: int
roads = [
Road(maximum_speed=25),
Road(maximum_speed=50),
Road(maximum_speed=80),
Road(maximum_speed=100),
]

So we have a list of four roads with different speeds.

Now consider this function:

def get_slow_roads():
return [
road for road in roads
if road.maximum_speed == 25
]

The function name already implies the business rule. Notice the hard-coded predicate in the heart of the function.

When using a framework like Django the function would look like this:

class Road(models.Model):
maximum_speed = models.IntegerField()
def get_slow_roads():
return Road.objects.filter(maximum_speed=25)

Notice the predicate here as an inherent part of the ORM.

Encapsulating predicates

One way — a common way — to optimise the code is to move the predicate to the Road class itself:

class Road:
maximum_speed: int
def is_slow(self):
return self.maximum_speed == 25

def get_slow_roads():
return [
road for road in roads
if road.is_slow()
]

With this optimisation you basically encapsulate the predicate. It’s moved from low level code — in the heart of the list comprehension — to a higher level, in de domain object itself.

Now when the slow speed limit changes, you only have to change the predicate in the domain object instead having to search for it throughout the whole codebase.

The is_slow function can also be tested more easily than on the lower level, because it’s a pure implementation of the predicate itself.

Django doesn’t really like this kind of functions on the model because this kind of functions can’t be converted to SQL. Therefore in Django you need to implement it on the model manager and basically call the same filter function there.

While encapsulating predicates like this is already a very good optimisation, it could be optimised even further.

Using the specification pattern

Let’s start with a code example:

@dataclass
class Road:
maximum_speed: int

def is_slow(self) -> bool:
return EqualsSpecification(
"maximum_speed", 25)
.is_satisfied_by(self)

Notice the predicate is now encapsulated by an EqualsSpecification object, which implements the is_satisfied_by function.

The is_satisfied_by function in this case is implemented as follows:

def is_satisfied_by(self, obj: Any) -> bool:
return getattr(obj, self.field) == self.value

At first glance, this might look like a lot of overhead. In this example, it might also be a bit of unnecessary overhead. But when using it together with, for example, the repository pattern, which is another design pattern as part of DDD, it suddenly becomes a very powerful tool.

The specification pattern is so powerful because it abstracts away the implementation in the environment, where the predicate needs to apply to.

If your collection of roads are stored in a regular list in Python, as shown in the example above, you need a different implementation of the predicate as when the collection is stored in SQL.

The specification pattern ensures you only have to write the predicate once and can be used differently regardless of the underlying environment.

Let’s apply the repository pattern and show how they are connected and thus how the specification pattern can be used.

Consider the following repository interface:

class RoadRepository(ABC):
@abstractmethod
def get_all(self, specification: Specification) -> List[Road]:
...

def slow_roads(self) -> List[Road]:
specification = EqualsSpecification("maximum_speed", 25)
return self.get_all(specification)

Notice the specification is part of the interface itself. This is a bit comparable to the Django model manager in a way, to be fair.

Now consider a Python list implementation of the road repository:

class PythonListRoadRepository(RoadRepository):
def __init__(self, roads: List[Road]):
self.roads = roads

def get_all(self, specification: Specification) -> List[Road]:
return [
road for road in self.roads
if specification.is_satisfied_by(road)
]

When we instantiate the repository, we can call slow_roads to get what we need:

road_repository = PythonListRoadRepository([
Road(maximum_speed=25),
Road(maximum_speed=50),
Road(maximum_speed=80),
Road(maximum_speed=100),
])
road_repository.slow_roads()

When we suddenly migrate from Python lists to Django, we just have to implement a different repository and convert our specification to something that works with the Django ORM:

class DjangoRoadRepository(RoadRepository):
def get_all(self, specification: Specification) -> List[Road]:
q = DjangoOrmSpecificationBuilder.build(specification)
return Road.objects.filter(q)

The builder would look like this:

class DjangoOrmSpecificationBuilder:
@staticmethod
def build(specification: Specification) -> dict:
if isinstance(specification, EqualsSpecification):
return {specification.field: specification.value}

Going on with Django, now we can move the specification to the domain object, regardless of the implementation behind (Python lists or SQL):

class Road(models.Model):
maximum_speed = models.IntegerField()

@staticmethod
def slow_roads_specification() -> Specification:
return EqualsSpecification("maximum_speed", 25)

And update the repository interface accordingly:

class RoadRepository(ABC):
@abstractmethod
def get_all(self, specification: Specification) -> List[Road]:
...

def slow_roads(self) -> List[Road]:
return self.get_all(Road.slow_roads_specification())

Now you don’t need an explicit Django model manager, but we implemented it as a repository.

Of course the example with moving the specification to the domain object will also work with a plain Python dataclass:

@dataclass
class Road:
maximum_speed: int

@staticmethod
def slow_roads_specification() -> Specification:
return EqualsSpecification("maximum_speed", 25)

Notice that we don’t have to change the repository interface in this case.

Conclusion

The specification pattern is a great tool to abstract business logic away from the low-level implementation. Even from the chosen application framework, like Django.

The specifications themselves are sustainable. They are implemented in pure Python without any dependency. This is very SOLID.

Using the specification pattern requires a bit of practice, as you need to think a bit in a different way. This is mainly due to the nature of dependency inversion of the SOLID principles.

The specification class and builder are part of an open source project I’m maintaining. You can find it on Github.

Feel free to reach out if you have any questions regarding this article or the Github project. I’m happy to help. You can reach out to me here or on my personal website.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store