Custom logic at the start — delegation

The next few pages address the problem of needing to re-use some logic from one view in another view. We’ve thought about how we can use utility functions and classes, but sometimes these don’t cut it — sometimes the majority of the body of the view needs to be re-used. How can we do that with FBVs?

Continuing our example of a list of products, let’s add a variation. As well as the main product list page, we’ve also got a “special offers” page — or rather, a set of them, because we have a SpecialOffer model that allows us to have many different ones. Each of these pages needs to display some details about the special offer, and then the list of products associated with that offer. Our feature requirements say this product list should have all the features of the normal product list (filtering, sorting etc.), so we want to re-use the logic as much as possible.

So our view will need to do two things: it will show a single object, and also shows a list. The answer of how to do two things with FBVs is: do two things. No special tricks needed for that. Let’s start with a simple version of our view:

# urls.py

from . import views

urlpatterns = [
    path('special-offers/<slug:slug>/', views.special_offer_detail, name='special_offer_detail'),
]
# views.py

def special_offer_detail(request, slug):
    special_offer = get_object_or_404(SpecialOffer.objects.all(), slug=slug)
    return TemplateResponse(request, 'shop/special_offer_detail.html', {
        'special_offer': special_offer,
        'products': special_offer.get_products(),
    })

I’ve assumed the SpecialOffer.get_products() method exists and returns a QuerySet. If you have an appropriate ManyToMany relationships the implementation might be as simple as return self.products.all(), but it might be different.

But now we want to change this view to re-use the logic in our normal product_list view, whether it is filtering/sorting/paging or anything else it has built up by now (which I’ll represent using the function apply_product_filtering() below). How should we do that?

One way would be to do what we did in Common context data — move part of the existing product_list view into a function that takes some parameters and returns the data to be added to the context. However, sometimes that interface won’t work. For instance, if the view decides that in some cases it will return a completely different kind of response — perhaps a redirection, for example — then the common logic won’t fit into that mould.

Instead we’ll use what I’m going to call delegation — our entry-point view will delegate the rest of the work to another function.

To create this function, look at our old product_list view and apply parameterisation. The extra parameters we need to pass are: the product list QuerySet; the name of the template to use; and any extra context data. With those in place we can easily pull out a display_product_list function, and call it from our two entry-point view functions:

def product_list(request):
    return display_product_list(
        request,
        queryset=Product.objects.all(),
        template_name='shop/product_list.html',
    )


def special_offer_detail(request, slug):
    special_offer = get_object_or_404(SpecialOffer.objects.all(), slug=slug)
    return display_product_list(
        request,
        context={
            'special_offer': special_offer,
        },
        queryset=special_offer.get_products(),
        template_name='shop/special_offer_detail.html',
    )


def display_product_list(request, *, context=None, queryset, template_name):
    if context is None:
        context = {}
    queryset = apply_product_filtering(request, queryset)
    context.update(paged_object_list_context(request, queryset, paginate_by=5))
    return TemplateResponse(request, template_name, context)

Note

For those unfamiliar with the signature on display_product_list:

  • the arguments after * are keyword only arguments.

  • queryset and template_name lack defaults (because we don’t have any good defaults) which forces calling code to supply the arguments.

  • for context we do have a sensible default, but also need to avoid the mutable default arguments gotcha, so we use None in the signature and change to {} later.

At the template level, we’ll probably do a similar refactoring, using include to factor out duplication.

That’s it! See below for some more discussion about how this delegation pattern might evolve. Otherwise, onto Custom logic in the middle — dependency injection.

Discussion: Function based generic views

What happens if you keep going with this parameterisation pattern? Let’s say you have not one model, but lots of models where you want to display a list, with the same kind of filtering/sorting/paging logic applied?

You might end up with an object_list function and a bunch of parameters, instead of product_list. In other words, you’ll end up with your own function based generic views, just like the ones that used to exist in Django.

Isn’t that a step backwards? I’d argue no. With the benefit of hindsight, I’d argue that the move from these function based generic views to class based generic views was actually the backwards step.

But that is in the past. Looking forward, the generic views you might develop will be better than both Django’s old generic FBVs and the newer generic CBVs in several ways:

  • They will have all the functionality you need built-in.

  • Importantly, they will have none of the functionality you don’t need.

  • You will be able to change them whenever you want, however you want.

In other words, they will be both specific (to your project) and generic (across your project) in all the right ways. They won’t suffer from Django’s limitations in trying to be all things to all men.

As FBVs they will probably be better for you than your own custom CBVs:

  • They will have a well defined interface, which is visible right there in the function signature, which is great for usability.

  • The generic code will be properly separated from the specific. For example, inside your object_list function, local variable names will be very generic, but these won’t bleed out into functions that might call object_list, because you don’t inherit local variable names (in contrast to classes where you do inherit instance variable names).

  • At some point you might find you have too many parameters to a function. But this is a good thing. For your class-based equivalent, the number of extension points would be the same, but hidden from you in the form of lots of mixins each with their own attributes and methods. With the function, your problem is more visible, and can prompt you to factor things out. For example, if you have several parameters related to filtering a list, perhaps you actually need to invent a Filterer class?

Discussion: Going further with generics

If you have a large number of views that are very repetitive, you may continue this pattern even further. Examples of projects that have done this are:

Both of these have their own forms of “Class Based Views”, but actually provide higher level functionality in terms of sets of views rather than just individual views.

I’ve had good experiences with both, and here are my ideas about why they have succeeded:

  • They both provide a fairly narrow set of views. Both are essentially CRUD based, and this means that the views are quite constrained in what they do.

    This is in contrast to a classic web app where a single view can do a very wide range of things, and could easily combine multiple different things.

  • Due to this constraint, they can provide abstractions that are higher level than a single view (for example, the ModelAdmin and the ViewSet classes). You can get a very large amount of functionality out of these classes “for free” — with just a small amount of declarative customisation. So when you need to go further and write some code, you are still way ahead of where you would have been without them.

  • They provide a lot of their functionality in terms of composing behaviour defined in other objects and classes, rather than by inheriting from mixins. For example, the Django admin has behaviour defined in other things like Form and ListFilter that are referenced from your ModelAdmin; DRF has separate classes for serializers, permissions and filtering that are referenced from your ViewSet.

Discussion: Copy-Paste Bad, Re-use Good?

I’ve claimed above that your own generic views would be better than the generic CBVs that Django provides, which leads to a question:

Where do Django’s generic CBVs come from? Why didn’t we stop with function based generic views?

The problem was that there was an endless list of requests to extend generic views to do one more thing, and we wanted to provide something more customisable.

Our answer to this problem ought to have been: if these generic views don’t do what you want, write your own. You can easily copy-paste the functionality you need and start from there. So why didn’t we just say that? I think we somehow had the idea that copy-paste is the ultimate disaster in software development. If there is some functionality written, we should always make it re-usable rather than re-implement, and we’ve somehow failed as software developers if we can’t.

You can see this in the design of the CBVs. A lot of the complexity in the hierarchy looks like it was introduced in order to avoid a single duplicate line. But it is knowledge and not lines of code that we should be trying not to duplicate. There are plenty of things worse than copy-paste programming, and the wrong abstraction is one of them.

I recently wrote several implementations of Mozilla’s Fluent localisation language, all of them in Python. First I wrote an interpreter, then a Fluent-to-Python compiler, then a Fluent-to-Elm compiler. These last two projects are clearly very similar in nature. So when I started the second of them, I did so with one big copy-paste job of 2500 lines of code. I knew that although there were many, many similarities between the two projects, there would also be many, many differences. I was right — the two code bases still share a huge amount in terms of structure. In a few places they even still have significant chunks of identical code. But the code bases have also diverged at many, many points, both in small details and in more fundamental ways.

The decision to copy-paste was overwhelmingly the right decision. Attempting to avoid duplication while I was developing the second would have been an absolute killer in terms of complexity, and may have failed completely. Once or twice I copied fixes or changes directly from one to the other, but most times when I had “equivalent” changes to do, they looked significantly different in the two projects. Having to do them twice from scratch cost far, far less than attempting to write the two projects with a common abstraction layer.

Before you can abstract commonality, you actually need at least two examples, preferably three, and abstracting before then is premature. The commonalities may be very different from what you thought, and when you have enough information to make that decision you might decide that it’s not worth it. So avoiding all duplication at any cost is not the aim we should have.

Discussion: Multiple mixins?

When doing a combined single object lookup with a list of objects, contrast the simplicity of the above FBV code with trying to wrangle CBVs into doing this.

These Django docs do come up with a solution for this case, but it is a house of cards that requires lots of extremely careful thinking and knowing the implementation as well as the interface of all the mixins involved.

But, after scratching your head and debugging for an hour, at least you have less typing with the CBV, right? Unfortunately, the opposite is true:

Here is our view implemented with Django CBVs — as it happens, it is exactly the same as the example in the docs linked above with model names and template names changed:

from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin

from shop.models import SpecialOffer


class SpecialOfferDetail(SingleObjectMixin, ListView):
    paginate_by = 2
    template_name = "shop/special_offer_detail.html"

    def get(self, request, *args, **kwargs):
        self.object = self.get_object(queryset=SpecialOffer.objects.all())
        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['special_offer'] = self.object
        return context

    def get_queryset(self):
        return self.object.products.all()

And here is The Right Way (including calling Paginator manually ourselves without any helpers):

from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse

from shop.models import SpecialOffer


def special_offer_detail(request, slug):
    special_offer = get_object_or_404(SpecialOffer.objects.all(), slug=slug)
    paginator = Paginator(special_offer.products.all(), 2)
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)
    return TemplateResponse(request, 'shop/special_offer_detail.html', {
        'special_offer': special_offer,
        'page_obj': page_obj,
    })

This is a clear win for FBVs by any code size metric.

Thankfully the Django docs do add a “don’t try this at home kids” warning and mention that many mixins don’t actually work together. But we need to add to those warnings:

  • It’s virtually impossible to know ahead of time which combinations are likely to turn out bad. It’s pretty much the point of mixins that you should be able to “mix and match” behaviour. But you can’t.

  • Simple things often turn into complicated things. If you have started with CBVs, you will most likely want to continue, and you’ll quickly find yourself rather snarled up. You will then have to retrace, and completely restructure your code, working out how to implement for yourself the things the CBVs were doing for you. Again we find the CBV is a bad starting point.