📜 ⬆️ ⬇️

My approach to Class Based Views

Luke Plant (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 the use of some fairly common templates (for example, when two forms are presented in the same view). And apparently, I'm 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, which I applied in one of the projects. This approach can be described in one phrase: " Create your own base class ."

With a fairly simple model view, using CBV in Django can save time. But in more complex cases, you will encounter a number of difficulties, at least you will have to immerse yourself in the study of documentation .
')
All this can be avoided, for example, by using a simplified CBV implementation . 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 Django View class is a great thing. This method was implemented after numerous discussions to facilitate isolation of the request by creating a new instance of the class to handle each new request. I gladly borrowed this idea.

Giving up bad ideas


Personally, I don’t like the dispatch method, because it involves completely different processing of 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 just to ignore certain data, this method requires writing additional code, which for me is a bug.

Therefore, I abandoned this method in favor of the simple function handle , which must be implemented when creating any logic.

Also, I don’t like that templates are automatically named based on model names, etc. This programming is by convention, which unnecessarily complicates life with the support of the code. After all, someone will have to frap to find out where the pattern is used. That is, when using such logic, you MUST KNOW where to look for information about whether a 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 impurity classes (mixins) and base classes. Due to the uniformity of the stack, I can not write crazy hacks to interrupt inheritance .

Writing the right API


Among other things, I don’t like forced verbosity in CBV Django when adding new data in 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 is usually still worse, since the data added to the context can be calculated using another method and hang on self so that get_context_data can find get_context_data . In addition, the more code, the easier it is to make a mistake. For example, if you forget about the call to 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 didn’t pay much attention to this until I realized that people use standard generators / snippets to create new CBVs ( example 1 , example 2 , example 3 ). If people need these tricks, it means that you have created a very cumbersome API.

I can advise: imagine what you would like to get an API, and implement it . For example, for a static add 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 context defined by ParentView , even if I do not explicitly call super . In the end, we almost always want to add data to context . And, if necessary, the subclass must remove the specific inherited data by assigning the key None .

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

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

Of course, nothing should be corrupted at the class level, and the isolation of the request should not be broken. In this case, all methods should work predictably and without any difficulty. At the same time, one cannot allow the possibility of accidental changes from within the method defined by the class of the dictionary of context .

When you finish dreaming, you will probably find that your imaginary API is too difficult to implement due to the nature of the language itself, you need to modify it somehow. Nevertheless, the problem is solved, although it looks a bit magic. Usually, the definition of a method in a subclass without using super means that the definition of the class super can be ignored, and you cannot use super at all in class attributes.

I prefer to do this in a more transparent way, using the name magic_context for the class attribute and method. So I don’t give a pig to those who will then maintain the code. If something is called magic_foo , then most people are curious about why it is “magic” and how it works.

The implementation uses several tricks, and first of all this: using reversed(self.__class__.mro()) , all super-classes and their magic_context attributes are magic_context , and the dictionary containing them is updated iteratively.

Please note that the TemplateView.handle method is extremely simple; it only calls another method that does all the work:

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

This means that the subclass defining the handle to execute the necessary logic does not need to call 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 series of hooks to handle things like AJAX validation when submitting a form, uploading RSS / Atom for list views, etc. This is quite simple, since I control the base classes.

Finally


The basic idea is that you are not bound to be limited to Django's capabilities. Nothing deeply related to CBV is integrated deep into it, so your own implementations will be no worse or even better. I recommend that you write exactly the code you need 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 Django CBV API. After all, your project will use a different set of base classes. However, the advantages still more than compensate for this inconvenience.

Source: https://habr.com/ru/post/274995/


All Articles