A controversial #Django topic:
class-based views (CBVs) or function-based views (FBVs)?
I started with function-based views, but after a few years of using Django I pretty much moved fully to class-based views. Both have pros & cons (and vocal advocates).
The Django docs introduce FBVs first and then discusses CBVs as well as Django's built-in "generic class-based views" (GCBVs).
Django-REST-Framework also makes heavy use of CBVs.
But class-based views CAN be confusing.
Some folks strongly prefer FBVs over CBVs.
I sympathize with that view (pun intended). I struggled with the transition to CBVs many years ago, but I eventually grew to appreciate their structure and brevity.
But CBVs do have pain points!
Using CBVs often means figuring out which attributes & methods to define/override.
#Django's "Generic" class-based views are even more confusing. GCBVs require understanding multiple inheritance because their code is spread out over many little classes.
Some #Django developers advocate sticking entirely to FBVs.
For example, see the unabashedly opinionated "Django Views — The Right Way" guide
https://spookylukey.github.io/django-views-the-right-way/#django-views-the-right-way
But many Django devs (including me) use class-based views.
I use CBVs because reading them is often pretty easy.
All my custom stuff lives in my class and all the standard cookie cutter stuff lives in the parent class(es).
Experienced GCBV users often find them pretty easy to understand at a glance.
But Django's GCBVs have some data that's implicitly determined (a bit magically) if not defined.
For example, the default template name for a DetailView is derived from the given model's class name. I prefer explicitly defining non-obvious data on my views.
Note that writing CBVs is often trickier than reading them, especially for GCBVs (which involve multiple inheritance ).
When defining a GCBV, I try to look at the defined methods and then override the ones I need to customize.
@treyhunner For me the template name thing is the best part of GCBVs; I have reimplemented them here for FBVs https://github.com/matthiask/feincms3/blob/main/feincms3/shortcuts.py#L72-L96
I find FBVs easier to read when a view e.g. includes a listing and also a form or something. I keep views themselves short by moving functionality into models, forms and utilities, so my opinionated view is that all CBVs do at this point is hide the control flow.
(The #DjangoJune series was mentioned on the Python People podcast. Thanks for your thoughts!)
@matthiask @treyhunner everyone always slides from the (specific) GCBVs (shipped with Django) to CBVs (in general) as if bad code depended on whether you used a class or not.
@carlton @treyhunner I'm not 100% sure I understand what you're saying. Bad code will always be bad. I don't think I specifically said that (G)CBVs are bad code. All I'm trying to say/share is that I prefer the more obvious control flow offered by FBVs.
It's the same as with the telephone: I like talking on the phone more when I am doing the calling
@matthiask @treyhunner If you’re not talking about the specific GCBVs I don’t see how CBVs hide the control flow. Your handler is called with the request. It’s job is to return the response. The only difference from a FBV is it’s nested in a level and you’ve got the self parameter, in exchange for which you get a namespace. (And if I need to handle multiple methods I get that level of nesting back.) So
@carlton @treyhunner Right. One can always argue everything and it definitely depends on personal preferences etc. but what I see as the view in CBV is the inner function here:
https://github.com/django/django/blob/c9b9a52edc66be117c6e5b5214fa788a4d5db7a8/django/views/generic/base.py#L96-L104
So, the view function instantiates the view class and hands off control to View.dispatch which in turns calls get/post/etc., and those methods call other methods in turn such as get_context_data etc. when all you wanted to do was "obj = get_object_or_404(); return render()".
@carlton I think there's a lot of value in teaching FBV first (as the tutorial does) and then move towards shortcuts and utilities instead of towards GCBV, because the latter may give people the impression that all the things GCBVs do have to happen when they don't.
I'm very aware that this is due to my own preferences and I don't want to criticize anyone for their choices! The difference isn't that big in the common case anyway. When it is, I'd rather use a FBV than override dispatch() though.
@matthiask oh, yes. I think there’s a lot in showing people request-response before the shortcuts pedagogically. But that again is about the GBCVs…
class MyView(View):
def get(self, request, *args, **kwargs):
return HttpResponse("Hello, World!")
@matthiask the extra machinery buys you the per-method separation. Nobody needs to be overriding dispatch. (I’ve done that a handful of times in all these years, always because I was being too clever.)
@carlton That mostly happens (or happened) when applying decorators in views.py (not urls.py) in the past.
Separating by HTTP method is fine but where do you put the common parts for all methods which you want to run before entering the method-specific parts? Without something like https://ccbv.co.uk/ or the source I'd be lost, but I can also go the route of 'if request.method == ...' which still works.
@carlton A different way to put it would be: Utilities and shortcuts are composable and you don't have to go the inheritance route for code reuse.
Or, I'd rather have a library than a framework if it can be similarly useful or ergonomic to use (very arguable, I know)
(I'm sure you know how much I love using the Django framework, so that's not a general point against frameworks.)
@matthiask So referencing CCBV is grist to my mill (I would say) It's great but only needed because the GCBVs that Django provides are so ludicrously factored (as well tested and doc'd and all the rest as they may be.)
TBF It's probably a matter of historical fact that they're the way they are only because the old GFBVs were so hairy. (I remember those, it wasn't fun.)
@matthiask
I haven't seen a modern take on GFBVs. I'm sure one would be possible.
My experience is that in team settings functional composition is harder to grok-on-average — and so harder to work on and maintain for all skill levels — than class-based "method object" style decomposition.
We can debate that over a beer sometime if you like.
Both styles allow writing bad, incomprehensible, code
I merely don't think it fair to point to a bad example of one kind, and say it applies to all.
@carlton I would love that! And I should probably also start writing a blog post .-)
@matthiask Yes! Show us what it would look like!
The concern is that the logic ends up in (e.g.) some decorator somewhere that I have to go and look up the implementation for, and work out in what order it’s applied, and … — in EXACTLY the same way as I now need to go look on CCBV in order to work out which bloody mixin is doing what…
In the ideal world we end up with a minimal set of both CBVs and FBVs — the CBVs with minimal inheritance, the FBVs with minimal composition.
@matthiask
Then we can look at them both and see that they’re equivalent, and that it’s just a matter of style.
@carlton I'm 99.99% sure that this will be the outcome. It's funny to me how programming is really more like an art or a craft than a science (even though natural science is also less science-y and much more creative than people generally think when they make these comparisons)
@matthiask Where’s that ?
@carlton Right now on my balcony
I should allow myself to travel more. Maybe an upcoming conference? I haven't been to a conference since Duth 2015 (or so) in Amsterdam.
@matthiask I hear @djangoconeurope is in Vigo next year
@carlton @djangoconeurope That sounds great!
I had to check. Only 25 hours when using public transports
@carlton I wrote a thing, here: https://406.ch/writing/composition-over-inheritance-the-case-for-function-based-views/
I could spend many more hours on this but I think it's "good enough"
@matthiask to
I’m guess I’m going to finally have to write up my take.
@carlton I'd love to read that!
@matthiask I can't tell if you do something special with your ModelForm classes or if there's a typo (comment -> instance) in `form = CommentForm(*data, comment=object)`. If you intended comment=, can you explain how you manage that over instance?
@CodenameTim Thanks! I'm often doing strange things like this (pass additional keyword arguments to ModelForm.__init__). The example is confusing and I fixed it here: https://github.com/matthiask/406.ch/commit/d1c214d2af27ab7c63d2b65632250e742091ac3c
@matthiask If you have a defined policy, I'm interested. I've definitely passed keyword arguments in myself, but have never formalized anything.
@CodenameTim @matthiask I am team requiring keyword-only args makes all code more readable and maintainable.
@CodenameTim Nothing written down but here's some terrible code which serves me well:
I pass in the request and the project (the project is the parent object of the logged hours object) and later use the project and/or request to initialize and check various fields. I assign fields on "self.instance" early which makes it possible to move more of the validation to the model. This is useful in this case because some of the objects in this code base ...
@CodenameTim ... are created in multiple places through multiple forms, and keeping all of the required validation sync would be very annoying and also brittle.
@carlton Oh, I remember them too. I was slightly sad when they were removed but I soon saw that it was a net positive. Overriding methods is certainly better than adding yet another argument to those functions.