published on

Sniffing the Architecture - Sentinel

In the first edition of this series, we’ve looked at the ominous singleton pattern. The singleton pattern can be useful in some languages, like C++ or Java, however, it doesn’t make too much sense in other ones, like Python. Today we are going to take a look at a design pattern that is more common in multiple languanges, the sentinel.

The definition

The sentinel design pattern is used for cases when we would like to indicate missing data.

The definition sounds quite simple, right? Why would there be a design pattern to solve this? Let’s imagine a super simple cache implementation in Python:

from typing import Any


class CacheMissException(Exception):
    pass


class Cache:

    def __init__(self):
        self.cache = {}

    def get_value(self, key: str):
        value = self.cache.get(key)
        if value is not None:
            return value
        raise CacheMissException

    def set_value(self, key: str, value: Any):
        self.cache[key] = value

The code is pretty straightforward. With the get_value function we are trying to fetch a value from the internal dictionary, and if we couldn’t find it, we raise a custom exception instead. What is the issue here? Let’s try it out in the shell!

>>> cache = Cache()
>>> cache.set_value('apple', 1)
>>> cache.get_value('apple')
1
>>> cache.get_value('pear')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in get_value
__main__.CacheMissException
>>> cache.set_value('pear', None)
>>> cache.get_value('pear')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in get_value
__main__.CacheMissException

What we can see is that if we would like to store a None in the cache, we encounter an issue where we still get an exception even though we’ve explicitly set the cache value. If Python it’s very easy to implement functions that have the return value of None (which might even be intended), so we should come up with a solution for this. Hence, the sentinel.

The idea behind the sentinel is that you create a custom object which is checked instead of a null value that is a language built-in. In practice:

from typing import Any


class CacheMissException(Exception):
    pass


class Cache:

    def __init__(self):
        self.cache = {}
        self.sentinel = object()

    def get_value(self, key: str):
        value = self.cache.get(key, self.sentinel)
        if value is not self.sentinel:
            return value
        raise CacheMissException

    def set_value(self, key: str, value: Any):
        self.cache[key] = value

Let’s try this one out:

>>> cache = Cache()
>>> cache.set_value('pear', None)
>>> print(cache.get_value('pear'))
None

Wonderful. Now we can set None in the cache!

When is this useful

The sentinel has the clear use-case for the cache. The Python implementation is short and elegant, however, other languages, like C++, use the sentinel to create halt conditions for iterators. Essentially, it can be anything, that is not a real value in your dataset.

Thank you for reading this post, until next time.