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

  • 4,152