Overview

Fallback Fields

Despite what it says on the tin, fallback fields are not a full prototype inheritance system. I wrote them to solve a particular problem, and the prototype inheritance idea is similar enough to help with understanding them, but it's not the same thing. So let's start instead with what fallback fields are for.

Motivation and overview

I designed fallback fields for an application for making reports containing chart graphics (line charts, bar charts, etc). Users of this system get given some predefined charts, which are configured by an administrator for their account. The fallback fields are the result of the interaction of two partially contradictory requirements:

  1. Users can (re)configure their predefined charts, changing everything from the colour scheme through to which data sources the chart is reporting on.
  2. Admins can push changes to the predefined charts (e.g., adding a new data source to a line chart).

These requirements are in partial conflict in the case where both a user and an admin make changes to the same predefined chart: whose changes take precedence? Prototype inheritance of individual attributes answers this question: the user gets the values they have set on any attributes they have touched, but they get the admin's version of any attributes they didn't explicitly set themselves. But this produces a new problem: what should we do when the changes are to different attributes but are inconsistent (e.g., the admin removes a data source, and the user configures the legend entry for that data source)?

Fallback fields provide some tools to handle this situation, by disconnecting an object from its prototype: this copies all fallback attribute values from the prototype into the object itself, and removes the prototype. The result is that further updates to the prototype will no longer be reflected in the (now disconnected) object. (In the case of the charts, when the user assigns to an attribute that can be in conflict with another we can disconnect the chart. This still allows them to freely edit attributes that cannot conflict with admin changes, for instance the position of the chart legend.)

One final note: fallback fields were designed for use with Django models, for which field attributes are always present on the objects but may have value None. On a Django model, a fallback field should access the prototype if the local value is None; outside of the Django context, it may be more appropriate to access the prototype if the attribute has not been set on the object (thus allowing None as an attribute value). The implementation allows both variants, via a flag.

Examples

First, a non-Django example:

class SimpleDoc(FallbackFieldsMixin, object):
    title = FallbackProperty('_title')
    content = FallbackProperty('_content', on_edit_disconnect=True)

    def __init__(self, prototype=None):
        self.prototype = prototype

The attribute title will work as with JavaScript prototype inheritance. Also content will inherit the prototype's value, but if content is ever set the object will be disconnected from its prototype (see the tests for examples).

FallbackProperty allows getters and setters like the builtin @property decorator. The getter is a bit different though: you provide a function that takes an argument, which will either come from the local object or its prototype. If you redefine the setter it becomes your responsibility to set the object's attribute. The property will handle disconnecting for you though, if you use on_edit_disconnect=True.

class AlwaysTitledDoc(FallbackFieldsMixin, object):
    title = FallbackProperty('_title')
    content = FallbackProperty('_content', on_edit_disconnect=True)

    @title.fallback_getter
    def title(self, title_or_none):
        return title_or_none or '<untitled>'
    @title.setter
    def title(self, new_title):
        if not new_title or not new_title[0].isupper():
            raise ValueError(u'Invalid title (must be non-empty and start with a capital)')
        self._title = new_title

    @content.setter
    def content(self, new_content):
        """
        Despite this setter, the object *will* be disconnected if you assign to its content.
        """
        self._content = new_content.strip()

    def __init__(self, prototype=None):
        self.prototype = prototype

Things look very similar in the Django context, except that (a) you have to explicitly define the Django fields (of course), and (b) you need to tell FallbackProperty to use None as the fallback marker:

class DjangoDoc(DjangoFallbackFieldsMixin, models.Model):
    prototype = ForeignKey('DjangoDoc', null=True)
    _title = CharField(max_length=200, db_column='title', null=True)
    _content = Text(db_column='content', null=True)

    title = FallbackProperty('_title', fallback_on_none=True)
    content = FallbackProperty('_content', on_edit_disconnect=True,  fallback_on_none=True)

Warnings

  • Compared to ordinary attribute access, fallback fields will be hellishly inefficient. If performance matters to you, just don't.

  • It seems unlikely anyone will try, but just in case: don't reuse the same FallbackProperty objects within a single class. For example:

    should_work = FallbackProperty('_title')
    will_not_work = FallbackProperty('_content')
    
    class NormalDoc(FallbackFieldsMixin, object):
        title = should_work
        content = FallbackProperty('_content')
    
    class WierdDoc(FallbackFieldsMixin, object):
        title = should_work
        content = will_not_work
        other_content = will_not_work
    

    The reason is that a FallbackProperty needs to know what attribute it 'lives under', to know what to look up on the prototype. The should_work object above keeps its name on NormalDoc separate from its name on WeirdDoc, but will_not_work cannot keep track of whether it is accessed as content or as other_content.