I came to Django development from much lower-level development — embedded software, device drivers, and system software. What has impressed me most about Django (and python in general) is the manner in which it guides you to do the right thing in terms of code construction. The framework and language naturally make you think about better ways to express your designs.
What took me a little longer to understand was the power of Django applications (apps for short). While the advantages of modular code are well known, how my own Django apps could be truly modular was not immediately apparent. But now I have some guidelines, thanks to personal experience.
Reduce the requirements of your applications to a minimum
I first came across James Bennett’s excellent video about reusable Django apps early in my work with the web framework. James makes this point early in his video, but it did not, at first, make much sense to me. My objection was “Hang on — the app I have written needs to do this and this — if it doesn’t do both, it’s useless to me.” But this was wrong. Just because an app doesn’t satisfy the full product requirements, does not mean it is useless as a building block — quite the contrary.
For example, we needed an app which created and controlled resources in an external system. However, only users with certain permissions could control those resources. Permission checking was such a critical part of the design, that it was baked-in from the start. It was only after many months that I realised that the permission logic was truly separate from the resource-control logic. Instead it is the glue app which should enforce the permissions, not the resource-control app itself.
Testing, testing, testing
A significant advantage of small apps is testing. I have found testing web applications comprehensively can be quite difficult. Every UI widget introduces another dimension to allowed input. Building a test infrastructure for a small, simple app, one with very well defined functionality, is much easier than for a larger app – and the difference is even larger than I had first expected.
An example might help to illustrate this point. Say you need an app which distributes data across the network to nodes in a group. Nodes communicate regularly with the app, updating information, and retrieving updated information about peer nodes in the same group. Obviously the format of that data is a critical part of your overall design and specification, so you might be tempted to bake that format into the app.
Don’t. Let the app simply control node membership, authentication, and the mechanics of the distribution. Write your models so they can act as base classes. It is the derived classes which supply the actual data and its form, decoupling the base app from the data format sent to the nodes. Why is this useful? Well, it allows you to create frameworks that test the base app using very simple test data, even as simple as single integers. This eases testing significantly, sometimes allowing you to spot errors in the implementation with a simple glance at the test results. But if you bake the data format into the base app, and the data format is complex e.g. encrypted and check-summed, you need to create extensive test data just to validate the base app functionality.
This brings me to my second guideline.
Create models that can act as base classes
You should write apps that can be used as base apps. Recently I wrote a simple logging app, which explicitly allows the models contained within to act as base classes — other apps can derive from these models, override or extend, or both, the model methods. In other words, your derived apps shouldn’t do this:
logger = LoggingClass()
They should do this:
def logger(*args, **kwargs):
# Perhaps don't call this if your method does something very different.
super(MyLogger, self).__init__(*args, **kwargs)
print "Some message"
Thinking about my code in this manner — as opposed to building classes with methods that are simply called by other code — was a subtle change. But it has improved my object-oriented design enormously. And the resultant app is simple, modular, testable, and highly functional.
Django apps are not libraries
It is this guideline I consider my biggest insight.
Abstraction layers, where one layer knows only about the interface (API) exposed by the other layer and nothing about the implementation, is a fundamental concept in software design. If two modules share the same API, then the calling code doesn’t need to change. Software libraries often work this way.
It was this concept that I applied to different Django apps — have the apps export the same models, and have each model export the same methods. It seemed sensible — the purpose of each app was to control a different Cloud provider — it would mean the glue app would not need to care about which provider it was communicating with.
It didn’t work. Well, not as I hoped, and I was disappointed with the low level of modularity achieved. Trying to come up with a common Model-method interface for differing apps was quite difficult. Furthermore, if the model methods were to have the same prototype, the various apps needed to import other common apps which defined the objects used as prototype parameters. The result was much more dependency between apps than I expected.
It also turned out that having the glue app “know”with which app it was communicating did not result in too much code. In reality most code in a given app calls functionality in the same app, so there is very little need for structures like this:
else if app2:
But accepting small amounts of app-switching code like this, in a couple of key places, allows the apps themselves to be much more self-contained, re-usable, and testable.