spookylukey / django-views-the-right-way

Opinionated guide to writing Django views
Other
183 stars 20 forks source link

CBV for organising code from different HTTP methods #8

Open cipang opened 4 years ago

cipang commented 4 years ago

Many thanks for your informative guide first of all.

One of the way I use CBV is to group related GET/POST/HEAD... code together. For example:

class BookingView(View):
  def get(self, request):
    # code to show the booking
  def post(self, request):
    # code to update the booking

instead of using a bunch of if statements in a FBV like this:

def booking_view(request):
  booking = Booking.objects.get(...)
  if request.method == 'POST':
    # code to show the booking
  elif request.method == 'HEAD':
    # ...
  # show the booking...
  return render(...)

What's your opinion on that? Any suggestions in this use case can be included in your guide?

spookylukey commented 4 years ago

Thanks for the question!

I agree that there is a certain kind of neatness to the CBV pattern you've highlighted. My practice is to write the code exactly as you have here in the second example. I try to keep the contents of each branch small, by moving logic into the model layer or into a Form or utility where appropriate.

The advantage of this is that common setup code between get and post becomes much easier - you can just re-use local variables. This to me is a big advantage, since there is almost always common logic, and communicating by setting variables on self is kind of ugly and harder to reason about.

The disadvantage is that you get a longer function, instead of several shorter methods. I don't worry about this too much, because I don't pay much notice to people who say "no function should be more than 5 lines long" etc. I prefer short function/methods when they are meaningful bits of logic, but not for the sake of it.

In terms of boilerplate, it's about the same.

If there isn't common logic, I might have 2 completely separate views and use require_POST and require_GET decorators. https://docs.djangoproject.com/en/3.1/topics/http/decorators/#django.views.decorators.http.require_POST

For the case where you have no common setup, but want the same URL, an interesting possibility might be to do something like this:

booking_view = dispatch_by_http_verb()

@booking_view.get
def booking_view(request):
    # do get thing

@booking_view.post
def booking_view(request):
    # do post thing

Or something similar. See https://docs.python.org/3.8/library/functools.html?highlight=singledispatch#functools.singledispatch for other inspiration on what it might look like.

Implementation of dispatch_by_http_verb is left as an exercise BTW! I'm not sure if I'd use this pattern but it might be worth exploring.

Here's another idea:


@dispatch_by_http_verb
def booking_view(request, booking_id):
   # Common setup
    booking_account = get_booking_account_from_request(request)
    booking = Booking.objects.for_account(booking_account).get(id=booking_id)
    return (booking_account, booking)

@booking_view.get
def booking_view(request, booking, booking_account):
    # do get thing

@booking_view.post
def booking_view(request, booking, booking_account):
    # do post thing

Here dispatch_by_http_verb takes the return args from the first function and passes them as parameters to the sub view functions. Perhaps a bit obfuscated?

Of course, if you really like the CBV pattern you've described, then I'd suggest you just use it :-)

jayqi commented 3 months ago

I had the same question and felt like this topic was conspicuously absent from the guide. I think the discussion in this thread would be a great addition.