Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

David Winterbottom

June 03, 2015
Tweet

More Decks by David Winterbottom

Other Decks in Programming

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. 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
  5. 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
  6. 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
  7. • HTTP handling • URL routing • middleware • views

    • templates • statics • models • forms How MVC breaks down into Django components Note, controller is not equiv to views
  8. 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
  9. views.py Large, bloated modules Imports running below the fold Massive

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

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

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

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

    layer, don’t shove everything in models Beware: service is a loaded word
  14. 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
  15. 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
  16. 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
  17. 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('/')
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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