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:
- Is split up into small apps with specific responsibilities
- 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