base.controller

Polymorphic Models

The Third Prong of a “Model-View-Controller” Paradigm

Model-View-Controller, or “MVC”, is a well-established paradigm for structuring complex projects. In essence:

  • the Model represents a piece of data
  • the View handles displaying that data
  • the Controller handles manipulating that data

As we all write code in Python, and the standard Python way of building useful web-apps is Django, let’s look at this from a Django perspective.

  • your Model is the Python class that represents a piece of data
    • in Django you write a class that derives from models.Model
  • your View is the Python class that displays your data for the user
    • in Django you write a class that derives from views.View
  • your Controller is…
    • flat out missing

The idea is that the Model class is instantiated once for each piece of data. Django is very good at mapping data between tables in a database and object-oriented classes in your code. This is about half of Django’s job, and is sometimes called an Object Relation Mapping, or ORM. (Other Python ORM systems exist besides Django, such as SQLAlchemy.)

Similarly, the View class can present your data to the user. This is the other half of Django’s job, and encompasses the entire HTTP request/response framework. Django even includes some sophisticated form processing to help receive changes to data and communicate them back to the model.

But high-level business logic, logic that understands your data, that’s up to you to write, and to squeeze into the system anywhere you can fit it. Often, we end up writing this stuff as methods on either the Model classes or the View classes, though it doesn’t really fit either.

Polymorphism

Django really shows its limits when you have polymorphic data – where a single Model class handles data of related but not identical types.

For example, imagine a Document class that is used to handle various types of documents. Very likely, your Document model has a type field that specifies one of various values, like “HTML Document” vs. “Markdown Document” vs. “Plain Text Document”. Depending on any specific Document’s type, the methods you write on your model need to branch into very different code paths.

DOCUMENT_TYPES = [
    ('HTML', 'HTML'),
    ('MD',   'Markdown'),
    ('TXT',  'Plain Text'),
]

class Document(models.Model):
  type = models.CharField(max_length=4, choices=DOCUMENT_TYPES)
  # ...other fields...
  # ...and methods to manipulate this Document...

This is classic polymorphism: all your Documents are stored in the same table, that’s nice, but most every method you might write is going to need a bunch of if-then cases to deal with the different types of documents.

How Controllers Make This Better

With our controllers, your model attaches to a namespace of polymorphic options. Then, with one syntactic step, you can ask any Document for it’s controller, and get back a different class depending on which type of Document it is.

Since we like our Enum class, we’re going to use it for our types of documents. As such, our example becomes this:

base.Enum.Define(('DOCUMENT_TYPE', 'DocumentTypes'), (
    ('Undefined',  None),
    ('HTML',       'HTML'),
    ('Markdown',   'MD'),
    ('Plain Text', 'TXT'),
))

class Document(base.ControllerMixin, models.Model):
  CONTROLLER_NAMESPACE = DocumentTypes
  CONTROLLER_NAME_PROPERTY = 'type'

  type = models.CharField(DocumentTypes.MaxTagLength(), choices=DocumentTypes.Choices())

  # ...other fields...

We also need to define some Controller classes:

class DocumentController(base.Controller):
  CONTROLLER_NAMESPACE = DocumentTypes
  # ... methods in common for all documents ...

class HTMLDocumentController(DocumentController):
  CONTROLLER_NAME = DOCUMENT_TYPE_HTML
  # ... methods for HTML documents ...

class MarkdownDocumentController(DocumentController):
  CONTROLLER_NAME = DOCUMENT_TYPE_MARKDOWN
  # ... methods for Markdown documents ...

class TextDocumentController(DocumentController):
  CONTROLLER_NAME = DOCUMENT_TYPE_PLAIN_TEXT
  # ... methods for plain text documents ...

Now whenever you have a Document instance, that instance has an extra property called controller. When you request that controller you get an instance of the specific Controller class that handles that Document‘s type.

doc1 = Document()
doc1.type = 'HTML'
print(doc1.controller)

doc2 = Document()
doc2.type = 'MD'
print(doc2.controller)

doc3 = Document()
doc3.type = 'TXT'
print(doc3.controller)

This will print something similar to:

<__main__.HTMLDocumentController object at 0x10425db38>
<__main__.MarkdownDocumentController object at 0x104239f98>
<__main__.TextDocumentController object at 0x104275128>

Note: the .controller property is cached on each instance after it is first created, hence why this example uses three different instances.

That’s it in a nutshell: Instead of trying to implement all your polymorphic logic in one class, you get to split it out to separate classes and our Controller picks the right one to use based on the model’s type.


Some Specifics

Referring to the Model Instance

Every instance of a Controller can refer to the model instance it’s attached to as .item

Additionally, it can also use the slugified name of the class. So in our example above, each controller instance has a .document attribute which is exactly the same as its .item

If you want to change this, the controller may override CONTROLLER_ITEM_NAME:

class DocumentController(base.Controller):
  CONTROLLER_NAMESPACE = DocumentTypes
  CONTROLLER_ITEM_NAME = `foobar`

These controllers would have .item and .foobar properties that both refer to the model instance.

Enums Not Required

The CONTROLLER_NAMESPACE does not have to be an Enum. Anything that supports “in” testing will work, which means you can use even a simple list if you’d like:

MY_DOCUMENT_TYPES = ['HTML', 'MD', 'TXT']

class DocumentController(base.Controller):
  CONTROLLER_NAMESPACE = MY_DOCUMENT_TYPES

Class Hierarchy As Namespace

Additionally, we have a trivial base class type called ControllerNamespace that you can derive a class from, and we’ll take sub-classes of that class as options in the namespace.

Default Controller

If a specific controller can’t be found for an instance of your model, the top-level controller for your namespace will be used.

For instance, if the MarkdownDocumentController did not exist and you had a markdown Document, the top-level DocumentController would be instantiated instead.

Similarly, if the Document specifies a .type that isn’t in the namespace at all, we will log a message to console about the problem, but will again use the top-level DocumentController.

Class Controller

It is absolutely possible to request a .controller for a model class instead of a class instance. The same logic is followed for finding the right controller to instantiate. The difference is the controller instance’s .item property is given a totally new instance of the model class, rather than a populated instance.

Separate Hierarchies

One key takeaway is that the class hierarchy of your controllers is totally separate from the class hierarchy of your models.

In fact, unless you’re using ControllerNamespace, the actual class hierarchies are irrelevant to how controllers function. It is fully possible for multiple different model classes to use the same controller namespace and thus the same controllers.


Back to “OctoBase”