How to write a View

How to write a View

If you quickly want to assess a Django developer's competence, look
at their views modules. Despite being one of the first Django components everyone learns, they are one of the hardest to master. Django projects worldwide suffer from bloated, unmaintainable views

It doesn't have to be this way. Attend this talk to learn the patterns and practices of clean, testable views. Years of view writing and reading will be distilled down to a series of guidelines exploring the art of the prefects views.py module.

52d39c7b27386ca98bc016119d95b8b8?s=128

David Winterbottom

June 03, 2015
Tweet

Transcript

  1. How to write a view David Winterbottom @codeinthehole Should have

    used title “Have I got views for you” Hello etc
  2. Views are core part of Django projects - Part 3

    of the polls tutorial Function or callable class that takes a request and returns a response - Easy! Or are they?
  3. From years of code review,… Other parts of django (models,

    forms) have more conventions - views are the wild west Judge a developer’s competence by their views.py Credit: http://www.osnews.com/story/19266/WTFs_m
  4. Let’s talk about design patterns (This Escher image is from

    the GoF Book cover)
  5. Separation of concerns 1974 Dijkstra (all goods things were invented

    in the 70s) Good SoC = modular Good SoC achieved through encapsulation and interfaces Encapsulation is information hiding SRP is SoC but in the small
  6. Model View Controller Design pattern interpretation of SOC Evolved from

    GUIS to server-side web to client-side web Web frameworks interpret this differently Adapted to different contexts, eg MVVM
  7. This is wikipedia image for MVC Not always obvious how

    to translate this into web
  8. Controller Model View Sequence diagram is clearer!

  9. Model Template View ? Django has its own interpretation of

    MVC called MTV. View has become Template / Controller become View confusing There’s a FAQ page on it https://docs.djangoproject.com/en/1.8/faq/general/#django-appears-to-be-a-mvc-framework-but-you-call-the-controller-the-view-and-the- view-the-template-how-come-you-don-t-use-the-standard-names
  10. • HTTP handling • URL routing • middleware • views

    • templates • statics • models • forms How MVC breaks down into Django components Note, controller is not equiv to views
  11. What is a view? • An adaptor: • Translates HTTP

    requests into domain calls • Translates domain responses into HTTP responses by… • …providing domain data to presentation layer
  12. Here’s what goes wrong

  13. views.py Large, bloated modules Imports running below the fold Massive

    methods Junk draw of stuff that doesn’t belong in this layer
  14. Leaky boundaries = pollution Domain: validation, business rules Persistence: manipulating

    model fields, calling save Presentation: adjusting data according to how it should be displayed
  15. Some of this is just discipline / comes with experience

    Django to blame? Blurs lines with model forms, active record etc
  16. models.py ≠ Model is a misleading word (business logic and

    persistence) -> leads to fat models syndrome
  17. domain/ models.py forms.py services.py … = Think of a domain

    layer, don’t shove everything in models Beware: service is a loaded word
  18. from ..service.service import Service service = Service() 4 different things

    all called service could have been one function Java brain - belief that everything needs to be class No-one is too good for functions
  19. View smells • Passing the request/response to a domain function

    • Validation logic • Manipulating or saving models* • Sending email, accessing network etc * http://www.dabapps.com/blog/django-models-and-encapsulation/ Develop a sense of taste Alarm bells should ring when you see these things
  20. CBGV - They are a pathway that you need to

    think carefully before walking too far Work fine for simple use-cases Be ready to retreat back to the safety of TemplateView, FormView, View
  21. Keep views boring! • Don’t do anything interesting • HTTP

    ↔ Domain Domain layer
  22. Pre-commit • Any domain logic? • Any persistence logic? •

    Any presentation logic?
  23. What would a RESTful API do? Mentally swap the layers

  24. What would a management command do?

  25. Example

  26. from django import http from django.views import generic from django.contrib

    import messages from . import forms, acl, platform class CreateAccount(generic.FormView): template_name = "new_account_form.html" form_class = forms.Account def dispatch(self, request, *args, **kwargs): if not acl.can_user_create_account(request.user): return http.HttpResponseForbidden() return super(CreateAccount, self).dispatch(request, *args, **kwargs) def form_valid(self, form): try: platform.create_account(**form.cleaned_data) except platform.ServiceUnavailable as e: form.add_error(None, unicode(e)) return self.form_invalid(form) messages.success(self.request, "Account created") return http.HttpResponseRedirect('/')
  27. from django import http from django.views import generic from django.contrib

    import messages from . import forms, acl, platform class CreateAccount(generic.FormView): template_name = "new_account_form.html" form_class = forms.Account def dispatch(self, request, *args, **kwargs): if not acl.can_user_create_account(request.user): return http.HttpResponseForbidden() return super(CreateAccount, self).dispatch( request, *args, **kwargs) def form_valid(self, form): try: platform.create_account(**form.cleaned_data) except platform.ServiceUnavailable as e: form.add_error(None, unicode(e)) Few imports from the domain layer Even nicer to package it up into a single package
  28. class CreateAccount(generic.FormView): template_name = "new_account_form.html" form_class = forms.Account def dispatch(self,

    request, *args, **kwargs): if not acl.can_user_create_account(request.user): return http.HttpResponseForbidden() return super(CreateAccount, self).dispatch( request, *args, **kwargs) def form_valid(self, form): try: platform.create_account(**form.cleaned_data) except platform.ServiceUnavailable as e: form.add_error(None, unicode(e)) return self.form_invalid(form) messages.success(self.request, "Account created") return http.HttpResponseRedirect('/') FormView - using external API to create something ACL - could be done with mixin/ decorator
  29. class CreateAccount(generic.FormView): template_name = "new_account_form.html" form_class = forms.Account def dispatch(self,

    request, *args, **kwargs): if not acl.can_user_create_account(request.user): return http.HttpResponseForbidden() return super(CreateAccount, self).dispatch( request, *args, **kwargs) def form_valid(self, form): try: platform.create_account(**form.cleaned_data) except platform.ServiceUnavailable as e: form.add_error(None, unicode(e)) return self.form_invalid(form) messages.success(self.request, "Account created") return http.HttpResponseRedirect('/') No “View” suffix No “Form” suffix My opinion… General principle - let the importing class rename if it needs to I’ve been doing this for ages, very few codebases have exploded
  30. return super(CreateAccount, self).dispatch( request, *args, **kwargs) def form_valid(self, form): try:

    platform.create_account(**form.cleaned_data) except platform.ServiceUnavailable as e: form.add_error(None, unicode(e)) return self.form_invalid(form) messages.success(self.request, "Account created") return http.HttpResponseRedirect('/') FormValid - no logic! make a call into domain layer Works with REST API / man. cmd
  31. return super(CreateAccount, self).dispatch( request, *args, **kwargs) def form_valid(self, form): try:

    platform.create_account(**form.cleaned_data) except platform.ServiceUnavailable as e: form.add_error(None, unicode(e)) return self.form_invalid(form) messages.success(self.request, "Account created") return http.HttpResponseRedirect('/') Domain exception Don’t let exception hop layers
  32. import mock import httplib from . import views VALID_FORM_DATA =

    { 'name': 'My new account', 'description': 'This is my new account', } @mock.patch.object(views, 'acl') @mock.patch.object(views, 'platform') def test_valid_submission_calls_platform(webtest, mock_acl, mock_platform): # Stub ACL test mock_acl.can_user_create_account.return_value = True # Make HTTP request response = webtest.post('/accounts/create', VALID_FORM_DATA) assert response.status_code == httplib.FOUND # Check domain called correctly mock_platform.create_account.assert_called_with(**VALID_FORM_DATA) Test controller layer using webtest webtest is a pytest fixture pytest! like when two kitkats fall out of the vending machine
  33. Separate concerns To wrap up: strive for clean layers

  34. Clean views • Mentally swap the layers to test assumptions

    • Don’t: • Manipulate models directly or call save() • Pass the request/response out of views.py • Know when to stop using CBGVs Things to keep you on the straight and narrow Clean layers / separate concerns
  35. Clean ‘model’ layer • Think domain layer, not models modules

    • Beware fat models • The interesting part Avoid fat models Avoid fat services.py Avoid fat anything
  36. 1.Be a Force for Good 2.Make Things Happen 3.Pay Attention

    4.Remove Friction 5.Take an Artful Approach 6.Build a Culture of Trust 7.Seek the Truth 8.Think Bigger Evan Williams guidelines for working at Twitter BE ARTFUL Writing good views takes care, careful thought You need to know when to back off, avoid gumption traps Know when to follow the rules and when to break them