DEV Community

Dane Hillard
Dane Hillard

Posted on • Originally published at easyaspython.com on

Mixins for Fun and Profit

This post originally appeared on Easy as Python in December 2016.


There are people out there who have the time, motivation, and resources to cook meals entirely from scratch. They'll turn flour, water, and eggs into pasta and turn cheese, cream, and spices into a sauce. These people make delicious food, but it takes time. Can you imagine how much longer it would take if they also milled the wheat, farmed the chickens, and milked the cows themselves?

When creating software, there's sometimes a limit to the depth we should go. When pieces of what we'd like to achieve have already been executed well by others, though, it makes a lot of sense to reuse them. Instead of milling wheat and raising cows, we can buy flour and butchered beef. We can start from scratch, but not scratch scratch.

One way to set ourselves up for success in object-oriented programming is through a concept called a mixin. In Python, mixins are supported via multiple inheritance. Mixins take various forms depending on the language, but at the end of the day they encapsulate behavior that can be reused in other classes.

The delineation between using true inheritance and using mixins is nuanced, but it comes down to the fact that a mixin is independent enough that it doesn't feel the same as a parent class. Mixins aren't generally used on their own, but aren't abstract classes either.

Suppose we've written a slew of Python software and our application is ready to launch. We're about to hit the button when someone comes over and says β€œwhat kinds of things are we logging?” Dear God, you think. We aren't logging anything! We could go ahead and add the following boilerplate to all our modules to get started with logging:

import logging

class EssentialFunctioner:
    def __init__(self):
        ...

        self.logger = logging.getLogger(
            '.'.join([
                self.__module__,
                self.__class__.__name__
            ])
        )

    def do_the_thing(self):
        try:
            ...
        except BadThing:
            self.logger.error('OH NOES')

That's not too bad, but imagine doing this to twenty or thirty classes. Not fun. How can mixins help? Have a look. Let's create a LoggerMixin that does the same work as above:

import logging

class LoggerMixin:
    @property
    def logger(self):
        name = '.'.join([
            self.__module__,
            self.__class__.__name__
        ])
        return logging.getLogger(name)

With this nicely encapsulated, we can now go around adding the following to our existing code:

class EssentialFunctioner(LoggerMixin):
    def do_the_thing(self):
        try:
            ...
        except BadThing:
            self.logger.error('OH NOES')

class BusinessLogicer(LoggerMixin):
    def __init__(self):
        self.logger.debug('Giving the logic the business...')

We wrote the functionality just once, but now we can use it everywhere! All we have to do is inherit from LoggerMixin and we can proceed using self.logger as if we'd set that up in EssentialFunctioner and BusinessLogicer! Pretty awesome.

There are certain places where heavy use of mixins can save a ton of time or cognitive load. A use case that came up recently was in a Django project where several class-based views needed to serve only traffic within our intranet. An existing middleware does this check, raising an Http404 if a request comes from outside the network. Adding that functionality to our classes was fairly easy, using decorator_from_middleware and method_decorator on the views' dispatch methods:

from django.utils.decorators import decorator_from_middleware, method_decorator
from django.views import generic

from app import middleware

network_protected = decorator_from_middleware(
    middleware.NetworkProtectionMiddleware
)

@method_decorator(network_protected, name='dispatch')
class SecretView(generic.View):
    ...

However, when I added a second piece of functionality I had to decorate those views too. I was getting a little tired of decorating.

Happy Holidays

Instead, I refactored the network protection integration into a mixin, allowing all our class-based views to inherit from this as they saw fit:

from django.utils.decorators import decorator_from_middleware, method_decorator
from django.views import generic

network_protected = decorator_from_middleware(
    middleware.NetworkProtectionMiddleware
)

class NetworkProtectionMixin:
    @method_decorator(network_protected)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)

class SecretView(NetworkProtectionMixin, generic.View):
    ...

A similar approach can be used for Django's permissions, authentication, and more. This is powerful, and when used correctly can result in wonderfully expressive code. It also reduces complexity, allowing easier unit testing and comprehension.

At the end of the day, mixins are a DRY way to architect code, abstracting potentially complex functionality so that developers can concentrate more fully on the task at hand instead of propping up all the pieces. Rejoice!

Have you used mixins in practice? I'd love to hear more.

You may also enjoy my other stories about building stuff in Django.


Top comments (2)

Collapse
 
md1023 profile image
Maxim Nikolaev

All logging mixins were cut out from our project recently due complexity of a custom logging functions. We had no ability to set log level, so we replaced custom solution with a structlog.

My practice shows mixins are sweet way to make your code beautiful and human readable. But most of the time those solutions get ugly because you start to perfect them and spend too much time that you could've spent on simpler less perfect solution.

Collapse
 
easyaspython profile image
Dane Hillard

Well said. Mixins are a sort of design pattern, and as with other design patterns should be used where the problem is a great fit. The logging example is potentially a poor use case in the real world, but we have at least one use case like the network protected view that is still serving us well. If, down the line, the requirements of that use case change we might notice some pains too. It's an evolving judgment call!