Encapsulated Django

 

Keeping your apps small, focused
and free of circular dependencies

 

 

David Seddon

david@seddonym.me

http://seddonym.me

This talk

  • What is encapsulation?
  • Making a contract
  • Encapsulation tactics

What is encapsulation?

How might it relate to Django?

"Encapsulation is the packing of data and functions into a single component."

 

- Wikipedia entry on Encapsulation

An Encapsulated Django Project is one that:

  1. Is split up into small apps with specific responsibilities
  2. Has no circular dependencies between its apps.

Me, just now

Settings

App

App

App

App

Encapsulated Django Project

Settings

App

App

App

App

Circular dependencies

Settings

 

 

App

 

 

App

App

App

App

Very large apps

Make a contract

Commit to no circular dependencies!

# settings.py

...

INSTALLED_APPS = (
    # Apps lower down the list should not know about apps higher up

    # A main app that pulls together behaviour from other apps.
    # Usually defines the home page, for example
    'apps.main',
    
    'apps.journeybooking',
    'apps.journeyapplication',
    'apps.journeyrequest',
    'apps.driver',
    'apps.customer',

    # Core functionality that all the apps use; if it’s a good candidate for a
    # reusable app on other sites, it should probably start off here
    'apps.core',

    'django.contrib.admin',
    'django.contrib.auth',
    [etc]
)

Using INSTALLED_APPS to document your dependency chain

Don't worry

about templates

  • Keep templates together at the project level
  • No need to respect the dependency chain

Encapsulation Tactics

Splitting models up using OneToOneFields

ParentModel

ChildModel


>>> parent = ParentModel.objects.first()
>>> something_useful = instance.child.get_something_useful()

parent = models.OneToOneField(ParentModel,
                   related_name='child')

Use signals to keep the logic in child apps.

from childapp import ChildModel


class ParentModel(models.Model):
    ...
    def save(*args, **kwargs):
        super(ParentModel, self).save(*args, **kwargs)

        ChildModel.objects.create(parent=self)
# childapp/receivers.py

...

@post_save.connect(sender=ParentModel):
def create_child(sender, instance, created, **kwargs):
    if created:
        ChildModel.objects.create(parent=instance)

Splitting models up using OneToOneFields

Monkey patching

Adding methods from within your child app

#  childapp/models.py

from parentapp.models import ParentModel


def _special_method(self):
    ...
    return something_useful


ParentModel.special_method = _special_method

Providing custom APIs

 

Design APIs within your parent app so child apps can hook into its behaviour.

Providing custom APIs

 

  • Use django.conf.settings
  • Imitate the admin site
  • Consider polymorphism

Techniques

Providing custom APIs

# settings.py
    
PLUGIN_NAMES = ('child',)


# childapp/models.py

class Child(models.Model):
    ...

    def get_something_useful_filter_kwargs(self):
        return {
            'child__my_field__status': 3,
        }
        

Using settings

Providing custom APIs

# parentapp/models.py

from django.conf import settings


class Parent(models.Model):
    ...
   
    def get_something_useful(self):
        # Default filter arguments
        filter_kwargs = {'status': 1}

        # Use our custom setting to allow other models
        # to add to the filter kwargs
        for plugin_name in settings.PLUGIN_NAMES:
            filter_kwargs.update(getattr(self, plugin_name)\
                          .get_something_useful_filter_kwargs())

        return Parent.objects.filter(**filter_kwargs)

Using settings

Providing custom APIs

# settings.py

PLUGIN_FORM = 'childapp.forms.ParentAndChildForm'


# parentapp/views.py

...

from django.utils.module_loading import import_string
from django.conf import settings


class ParentView(views.CreateView):
    form_class = import_string(settings.PLUGIN_FORM)
    model = Parent

Using settings

This is how easy it is to define settings that import classes.

Providing custom APIs

# settings.py
     
 PARENT_URL_INCLUDES = (
    ('childapp.urls', 'child'),
    ('anotherchildapp.urls', 'anotherchild'),
 )
     
 

# parentapp/urls.py

urls = [
   # Parent app's urls here
]

# Add urls for child apps
for include_path, url_component in settings.PARENT_URL_INCLUDES:
    urls += url(r'^%s/' % url_component, include(include_path))

Using settings

Another example, this time with URLs:

Providing custom APIs

# childapp/admin.py
    
from django.contrib import admin
from .models import Child

class ChildAdmin(admin.ModelAdmin):
    list_display = ('my_field',)
    
admin.site.register(Child, ChildAdmin)

Imitate the admin site

# childapp/custom_plugins.py

from parent import custom_plugins, CustomPlugin
from .models import Child


class ChildPlugin(CustomPlugin):

    model = Child
    useful_fields = ['field_one', 'field_two']

    
custom_plugins.register(ChildPlugin)

Admin

'Custom plugins'

Providing custom APIs by imitating admin

# parentapp/__init__.py
    
from class_registry import Registry
from django.utils.module_loading import autodiscover_modules
from django.apps import AppConfig

custom_plugins = Registry()

class CustomPlugin(object):
    model = None
    useful_fields = []


default_app_config = 'parentapp.Config'
         

class Config(AppConfig):

    name = 'parentapp'
    verbose_name = 'Parent'

    def ready(self):
        # Automatically import any custom_plugins.py file in an app.
        autodiscover_modules('custom_plugins')

pip install django-class-registry

Providing custom APIs by imitating admin

# parentapp/models.py

from . import custom_plugins
   

class ParentModel(models.Model):

   ...
    
   def get_all_useful_fields(self):
        useful_fields = []
        for plugin in custom_plugins.values():
            useful_fields.extend(plugin.useful_fields)
        return useful_fields

Providing custom APIs using polymorphism

Driver job

Bar staff job

Job

Cleaner job

Summary

Commit to encapsulation

  • Make a contract using INSTALLED_APPS
  • Don't bother encapsulating templates

 

Some tactics

  • Split models up using OneToOneFields
  • Monkey patching
  • Make your own APIs, using:

 

  • Custom settings
  • A registry (imitate django.contrib.admin)
  • Polymorphism

David Seddon   http://seddonym.me

Encapsulated Django

By David Seddon

Encapsulated Django

  • 3,239