It’s easy to take best practices and good design patterns for granted. If everything just works, you don’t even realize that they are there. That is, until you sabotage them without knowing you did.
It pays off to know what hidden decisions shape the environment you are working in.
“Of course”, you might say - but chances are that you are not aware of many design principles which have shaped your favorite language and web framework.
Well, At Least I Wasn’t
I only realized this recently, after getting back into Django. Over the past few years, I have immersed myself in the worlds of containers, infrastructure work (aka DevOps) and service-oriented Golang architecture. I had the opportunity to make new, exciting mistakes and learn a ton in the process.
Taking these experiences back to Django has changed my view on the framework and the projects people build with it. Some details I would have ignored a few years ago became very present.
I’d Love to Share My Perspective
In this article, I would like to share a few topics which have helped me to spot and appreciate some of Django’s design philosophies with fresh eyes.
Sometimes, a new point of view can help you to level-up your skills faster and to make better-informed decisions in your future projects. I hope that they will help you get a new perspective on some aspects of Django’s design decisions, and things you should reconsider.
If you lack of awareness and appreciation for good design patterns, you’re bound to make bad decisions which will cause you unnecessary trouble down the line. On the other hand, you can avoid some common bad practices with a bit of knowledge and attention.
What I Learned to Appreciate
If there’s one thing that I learned to avoid, it’s complexity. It will come back to bite you, if you don’t make sure to isolate it and keep it under control.
I learned to love simplicity and separation of concern on an architecture and design level. The main points I encountered can be boiled down to:
- Keeping it simple (KISS)
- Loose coupling
- Explicit interfaces
- Clear responsibilities & boundaries
Those points might not seem like much. Almost obvious even. And that’s the point!
They fit in so well into Django, that they can be hard to spot. But it’s also easy to go against them by accident, simply because the awareness is not necessary most of the time.
I don’t think they should be taken for granted, nor lost out of sight for too long. A lost of questions can be answered by referring back to these.
This appreciating got shaped by different influences. I’d like to tell you briefly about each, and contrast it against aspects of working with Django which appear to be relevant.
Unix Philosophy
My favorite point of the unix philosophy is “Do One Thing And Do It Well.”
Instead of one single thing doing everything, you are using small tools. Each one has a clear function. The tools are built in such a way, that they can be chained together and use each other’s results to get a task done via cooperation.
Let’s look at a few prominent Django building blocks which are in sync with the “clear responsibility” idea:
- Each Django app should be built around one single responsibility
- Models (and managers) are where the business logic should reside
- Views are supposed to be as slim as possible
- Templates are only responsible for displaying data passed to them
The same goes for apps - if you can’t describe each of your Django app in one concise sentence, it’s a good sign that you might want to split it up.
There is however one thing, where I can’t help but feel a bit cautious about. It’s the fact that data model definitions and persistence concerns are coupled, and then populated with business logic. It’s not a bad thing, and not a concern for most Django projects. But having worked with some heavy and critical business logic, I really enjoyed the fact that it was decoupled from persistence concerns, and the details of data models.
Golang Interfaces
Go gives you a lot of flexibility, but is also built in a way which makes complexity easy to spot and hard to hide on a language level.
Go is a statically-typed language. You can’t go around and pass any type you want. But, you can use “interfaces” to tell a function to accept any type which provides a given set of function signatures.
Golang’s interfaces are a beautiful design tool once you get a good intuition for them. The trick is, to keep them as small as possible, and only use them in places where it should be possible to interchange parts.
If a type provides all function signatures which an interface requires, it “satisfies” that interface. You can tell a function to accept any type which satisfy one particular interface. It doesn’t matter what exact type hides behind the interface, as long as it provides the necessary function signatures.
If you have to define an interface first, and declare its use explicitly, it’s easier to be conscious of:
- What usage pattern you intend to make possible
- The interface surface - whether it’s too large or just small enough
- The complexity an interface can introduce - sometimes it’s just overkill
It’s also easier to see complexity in your design, and the where an emphasis on testing might help.
Django and Python both have “explicit is better than implicit” at their core, but that’s not always the case. Python makes duck typing possible by default, which can make it harder to appreciate the flexibility you have at your disposal. Sometimes, you don’t know what interfaces are in place. Generic class-based views come to mind as an example, as you can’t get around having to read the source code to understand what’s really going on and what can be overridden eventually.
Hexagonal Architecture
Hexagonal architecture is similar to many other concepts, clean architecture among them.
At the core, it’s a way to structure your project which makes loose coupling and controlling complexity easier.
Here’s a quick overview, but please refer to the links above for a better explanation:
- Your code is structured in (conceptual) layers.
- There’s a inner-most layer at the center, and layers around it (like an onion).
- Communication only goes on in between layers which are next to each other.
- In any given layer, you can only import from layers which are more internal. The import arrows only point inwards.
- Each layer is accessed through an explicitly defined interface.
The domain-specific logic lives at the core of your application. It does not care about the outside world. Around it, are less and less abstract components which take care of talking to the outside world, persisting data and the like. They are easy to substitute for others (via the concept of ports and adapters), as the way they are used is clearly defined, and there shouldn’t be deep connections apart from the well-defined interfaces.
This makes it possible to have a very clear separation of concerns, and to exchange the content of layers easily.
Django completely embraces loose-coupling. The layers of the framework don’t necessarily need to know about each other in detail - they only need to interact with the public interfaces of the parts they actually need. The same applies to different apps within a Django project. They mostly don’t need to know about all the other parts out there. This helps to limit the scope of complexity to single applications, and hopefully makes it harder to write convoluted spaghetti code if done right.
And Then There Are Deployments
Deployment and Django is a topic which is receiving surprisingly little attention. It’s also an example for a challenge which sometimes leads to people ignoring good design principles like clear interfaces, good boundaries, separation of concerns.
When dealing with deployments, you want to make your life as easy as possible. Operational complexity is a sure-fire way to trouble, which is why KISS can save you a lot of time and trouble.
It’s a complex topic, and I would like to focus on one single aspect: does your application need to care about where and how it’s being deployed?
If we take loose coupling and separation of concerns to heart, the clear answer here is no. Your web app shouldn’t care much where it’s deployed, or how it’s being deployed.
All it needs, is to get the necessary hints to know where the parts it needs are located. This usually happens by providing it with environment variables. You can think of environment variables as a configuration interface. It’s pretty universal and can be handled by almost any deployment method out there.
Isolating your Django application away from the way it’s deployed, and the process of deployment as far as possible makes sure that there’s a healthy boundary between your project, the deployment workflow and the environment to which it will be deployed. As soon as you are letting your Django project know too much about deployment matters, or even take responsibility which is outside of what a web application should care about, you’re introducing unnecessary and avoidable complexity into your life.
Making sure that your toolchain takes care of deployment matters, instead of burdening your Django project with the responsibility will make it easier for you to think about the task and fix issues.
In Conclusion
I hope that my perspective helped you to spot how some design principles apply to working with Django. While we only talked about a few of them, of course there are more which are just as interesting. Probably some important ones I’m not aware of.
Sometimes, it’s hard to spot good design choices. If you haven’t encountered them personally, and learned from experience what happens if they are missing, you can’t tell when you are breaking the rules and causing trouble for yourself down the line.
Being conscious of best practices, and noticing when you are about to contradict them can save you a lot of trouble in the long run. I hope you will be able to spot loose coupling, explicit interfaces and separation of concerns more clearly in the future, and to use them to your advantage.