Slide 1

Slide 1 text

How to write a view David Winterbottom @codeinthehole Should have used title “Have I got views for you” Hello etc

Slide 2

Slide 2 text

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?

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Let’s talk about design patterns (This Escher image is from the GoF Book cover)

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

This is wikipedia image for MVC Not always obvious how to translate this into web

Slide 8

Slide 8 text

Controller Model View Sequence diagram is clearer!

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

• HTTP handling • URL routing • middleware • views • templates • statics • models • forms How MVC breaks down into Django components Note, controller is not equiv to views

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Here’s what goes wrong

Slide 13

Slide 13 text

views.py Large, bloated modules Imports running below the fold Massive methods Junk draw of stuff that doesn’t belong in this layer

Slide 14

Slide 14 text

Leaky boundaries = pollution Domain: validation, business rules Persistence: manipulating model fields, calling save Presentation: adjusting data according to how it should be displayed

Slide 15

Slide 15 text

Some of this is just discipline / comes with experience Django to blame? Blurs lines with model forms, active record etc

Slide 16

Slide 16 text

models.py ≠ Model is a misleading word (business logic and persistence) -> leads to fat models syndrome

Slide 17

Slide 17 text

domain/ models.py forms.py services.py … = Think of a domain layer, don’t shove everything in models Beware: service is a loaded word

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Keep views boring! • Don’t do anything interesting • HTTP ↔ Domain Domain layer

Slide 22

Slide 22 text

Pre-commit • Any domain logic? • Any persistence logic? • Any presentation logic?

Slide 23

Slide 23 text

What would a RESTful API do? Mentally swap the layers

Slide 24

Slide 24 text

What would a management command do?

Slide 25

Slide 25 text

Example

Slide 26

Slide 26 text

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('/')

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Separate concerns To wrap up: strive for clean layers

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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