Nam Ngo's blog

Musings of a Software Developer.

Django Inspired Design Patterns - Part 1: Middleware

Refactoring code is (most of the time) a fun activity until you hit an obstacle or run out of ways to simplify the code. You’ll find yourself looking for new design patterns from books or even digging inside the source code of your favorite open source projects to find some inspiration. Having worked with Django for 3 years, I find its design philosophy and decisions ideal. As a result, I’ve decided to dedicate a blog series to cover this topic: Django inspired design patterns and how you can use them to refactor your code.

Middleware
Middleware is a simple yet very robust component of Django. Middleware is a framework of hooks into Django’s request/response processing. It’s a light, low-level “plugin” system for globally altering Django’s input or output. This should look familiar to you

settings.py
1
2
3
4
5
6
MIDDLEWARE_CLASSES = (
    'django.middleware.gzip.GZipMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
)

The core concept of middleware is to dynamically load middleware classes using the provided dotted paths.

Where shall we consider this design pattern?
Let’s consider a simple example of an email validator that all of us can relate to. The email validator:
1. Has input and output
2. Consists of validation conditions (validate input) - validators
3. Can optionally mutate the output - mutators

A simple version can be written like this

email_validator.py
1
2
3
4
5
6
7
8
9
10
 def validate_email(email):
     if '@' not in email:
         raise ValidationError('Invalid email. @ not found')
     if '.' not in email:
         raise ValidationError('Invalid email. Incorrect domain?')

     # lowercase domain
     name, domain = email.split('@')
     email = '@'.join([name, domain.lower()])
     return email

How do we go about refactoring?
Let’s break down the logic into small units of validators and mutators. We need to define a PIPELINE constant which holds the dotted paths that can be loaded.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
PIPELINE = (
    'validation.contains_dot',
    'validation.contains_at',
    'mutators.lowercase_domain',
    'mutators.lowercase_name',
)

# in validation.py
def contains_dot(email):
    if not '.' in email:
        raise Exception('No . found')
    return

def contains_at(email):
    if not '@' in email:
        raise Exception('No @ found')
    return

# in mutators.py
def lowercase_domain(email):
    value, domain = email.split('@')
    domain = domain.lower()
    return '@'.join([value, domain])


def lowercase_name(email):
    value, domain = email.split('@')
    value = value.lower()
    return '@'.join([value, domain])

Let’s create a EmailValidator class that dynamically loads validators and mutators. load_pipelines imports functions from dotted paths and add them into a list. This example uses __import__ but it’s recommended that you use importlib

email_validator_class.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class EmailValidator(object):

    def __init__(self, email):
        self._pipelines = []
        self.load_pipelines()
        self.email = email

        for func in self._pipelines:
            # execute each function
            res = func(self.email)
            if res:
                self.email = res

    def load_pipelines(self):
        for line in PIPELINE:
            mod_name, func_name = line.split('.')
            mod = __import__(mod_name)
            func = getattr(mod, func_name)
            self._pipelines.append(func)

email = EmailValidator('sOmeEmail@CamelCase.com').email

Why do it this way? What are the benefits?
- Pluggable: this design makes it easy for other developers to extend the logic of your code, it’s maintainable. Let’s say if there’s a new requirement to validate that the email is actually valid and will not bounce, we can easily add a new function to the validators file and add it into the PIPELINE.
- Loose coupling: each pipe (function) doesn’t know/care what the other does.
- Easy to unit test: by breaking down the logic into small units, it’s now easier to write tests. We can now write tests for each of those functions in the pipeline.

Comments