My approach to Class Based Views

Original author: Luke Plant
  • Transfer
Luke Plant is a freelance programmer with many years of experience, one of the key developers of Django.

I once wrote about my dislike of Class Based Views (CBV) in Django . Their use significantly complicates the code and increases its volume, while CBVs prevent some fairly common templates from being used (say, when two forms are presented in one view). And apparently, I am not the only one of the Django developers who adheres to this point of view.

But in this post I want to talk about a different approach that I applied in one of the projects. This approach can be described with one phrase: " Create your own base class ."

With a fairly simple model view, using CBV in Djangocan save time. But in more complex cases, you will encounter a number of difficulties, at least you have to immerse yourself in the study of documentation .

You can avoid all this, for example, using a simplified implementation of CBV . Personally, I went even further and started from scratch, writing my own base class, borrowing the best ideas and implementing only what I needed.

Borrowing good ideas


The as_view method provided by the class View in Django is a wonderful thing. This method has been implemented after numerous discussions to facilitate isolation of the request by creating a new instance of the class to handle each new request. I took this idea with pleasure.

Refusing Bad Ideas


Personally, I do not like the method dispatch, because it involves completely different processing GET and POST, although they often overlap (especially in cases of processing typical forms). In addition, when viewing rejected POST requests, when it is enough to simply ignore certain data, this method requires writing additional code, which is a bug for me.

Therefore, I abandoned this method in favor of a simple function handlethat must be implemented when creating any logic.

Also, I don't like the fact that templates are automatically named based on model names, etc. This is programming by convention, which unnecessarily complicates life with code support. After all, someone will have to go to find out where the template is used. That is, when using such logic, you MUST KNOW where to look for information on whether the template is used at all and how it is used.

Stack alignment


It is much easier to manage a relatively uniform set of base classes than a large set of mixins and base classes. Due to the uniformity of the stack, I can not write crazy hacks to interrupt the inheritance .

Writing the right API


Among other things, in CBV Django, I don’t like forced verbosity when adding new data context in fairly simple situations when instead of one line you have to write four:

class MyView(ParentView):
    def get_context_data(self, **kwargs):
        context = super(MyView, self).get_context_data(**kwargs)
        context['title'] = "My title"  # Это единственная строка, которую я хочу написать!
        return context

In fact, it’s usually even worse, because those added to the context data can be calculated using another method and hang on selfso that they can be found get_context_data. Also, the more code, the easier it is to make a mistake. For example, if you forget about the challenge super, then everything can go awry.

Searching for examples on Github, I reviewed hundreds of code samples like this:

class HomeView(TemplateView):
    # ...
    def get_context_data(self):
        context = super(HomeView, self).get_context_data()
        return context

I did not pay much attention to this until I realized: people use standard generators / snippets to create new CBVs ( example 1 , example 2 , example 3 ). If people need such tricks, it means that you have created too cumbersome APIs.

I can advise: imagine what API you would like to receive, and implement it . For example, for static adding in, context I would like to write this:

class MyView(ParentView):
    context = {'title': "My title"}

And for dynamic adding:

class MyView(ParentView):
    def context(self):
        return {'things': Thing.objects.all()
                          if self.request.user.is_authenticated()
                          else Thing.objects.public()}
    # Или, возможно, используя lambda:
    context = lambda self: ...

I would also like to automatically accumulate any contextthat is detected ParentView, even if I do not call it super explicitly. In the end, we almost always want to add data to context. And, if necessary, the subclass should remove specific inherited data by assigning a key None.

I would also like to be able to directly add data to context for any method in my CBV. For example, setting up / updating an instance variable:

class MyView(ParentView):
    def do_the_thing(self):
        if some_condition():
            self.context['foo'] = 'bar'

Of course, while nothing should be spoiled at the class level, and the isolation of the request should not be broken. Moreover, all methods should work predictably and without any difficulties. But at the same time, one cannot allow the possibility of a random change from within the method defined by the dictionary class context.

When you finish dreaming, you will probably find that your imaginary API is too difficult to implement due to the peculiarities of the language itself, you need to modify it somehow. However, the problem is solvable, although it does seem a little magic. Usually, defining a method in a subclass without using supermeans that superyou can ignore the class definition , and you cannot use it in class attributes at all super.

I prefer to do this in a more transparent way, using the name for the class attribute and method magic_context. So I do not add a pig to those who will then support the code. If something is called magic_foo, then most people will wonder why it is “magic” and how it works.

In the implementation , several tricks are used, and first of all this: with the help of reversed(self.__class__.mro())all super-classes and their attributes are extracted magic_context, and also the dictionary containing them is iteratively updated.

Note that the TemplateView.handle method is extremely simple, it only calls another method, which does all the work:

class TemplateView(View):
    # ...
    def handle(self, request):
        return self.render({})

This means that the subclass defining handle to execute the necessary logic does not need to be called super. It is enough for him to directly call the same method:

class MyView(TemplateView):
    template_name = "mytemplate.html"
    def handle(self, request):
        # логика здесь...
        return self.render({'some_more': 'context_data'})

In addition, I use a number of hooks to handle things like AJAX validation when submitting a form, RSS / Atom loading for list views, etc. This is pretty simple as I control the base classes.

Finally


The basic idea is that you do not have to limit yourself to the capabilities of Django. Nothing that relates to CBV is deeply integrated into it, so your own implementations will be no worse, or even better. I recommend that you write exactly the code that is needed for your project, and then create a base class that will make it work .

The disadvantage of this approach is that you will not facilitate the work of programmers who will support your code if they have learned the API for Django CBV. After all, your project will use a different set of base classes. However, the benefits still more than compensate for this inconvenience.

Also popular now: