$30 off During Our Annual Pro Sale. View Details »

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

    View Slide

  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?

    View Slide

  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

    View Slide

  4. Let’s talk about design patterns

    (This Escher image is from the GoF Book cover)

    View Slide

  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

    View Slide

  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

    View Slide

  7. This is wikipedia image for MVC

    Not always obvious how to translate this into web

    View Slide

  8. Controller Model View


    Sequence diagram is clearer!

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  12. Here’s what goes wrong

    View Slide

  13. views.py
    Large, bloated modules

    Imports running below the fold

    Massive methods

    Junk draw of stuff that doesn’t belong in this layer

    View Slide

  14. Leaky boundaries = pollution

    Domain: validation, business rules

    Persistence: manipulating model fields, calling save

    Presentation: adjusting data according to how it should be displayed

    View Slide

  15. Some of this is just discipline / comes with experience

    Django to blame?

    Blurs lines with model forms, active record etc

    View Slide

  16. models.py

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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




    View Slide

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

    View Slide

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

    View Slide

  24. What would a
    management
    command do?

    View Slide

  25. Example

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  33. Separate
    concerns
    To wrap up: strive for clean layers

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide