How to make Django Manager and Model's interactions follow the Open/Closed Principle?
I am designing models for my Django App and am concerned with decoupling the caller's logic from the model's implementation, so that future changes to the model itself do not require changes downstream in the codebase. In short adhering to the Open/Closed Principle (OCP).
I am wondering what is a best practice to implement this while leveraging Django's framework best.
Conceptually, I believe it would make sense to do the following:
from django.db import models
class FooManager(models.Manager):
def is_active(self):
return self.filter(is_active=True)
class Foo(models.Model):
_bar = models.CharField(max_length=100, db_column="bar")
is_active = models.BooleanField(default=True)
objects = FooManager()
@property
def bar(self):
return self._bar
@bar.setter
def bar(self, value):
if not self._bar:
self._bar = value
else:
#some setter logic
pass
Custom Manager for Query Logic For each model, a custom Manager is defined and is responsible for handling all sorts of filtering logic on the model. E.g. instead of calling
Foo.objects.filter(is_active=True)
, caller would callFoo.objects.is_active()
. In the future, if the implementation logic of is_active changes, the caller won't need to change it's logic, keeping things OCP.Encapsulation of model fields For each field in a model,
@property
are defined for getters and setters allowing to change the underlying model fields.Minimal coupling Coupling is only kept on the Custom Manager and the Model.
My main concerns with this approach
For point 1: Requiring all filtering logic to be defined as methods in the custom Manager could potentially lead to dozens of methods per manager (due to combination of fields), making it hard to manage.
For point 2:
While convenient when calling the model attribute e.g. Foo.bar
, the underlying field needs to be called when using the manager. e.g. Foo.objects.filter(bar=x)
won't work, need to use Foo.objects.filter(_bar=x)
. This requirement undermines the reason for point 2 unless point 1 is implemented as well.
Questions
Is this approach a good practice for achieving decoupling in Django while adhering to the Open/Closed Principle?
Are there better or more standard approaches within the Django ecosystem to decouple model logic and caller logic without introducing unnecessary complexity?
How do experienced Django developers handle encapsulation of fields while ensuring that queries remain intuitive and maintainable?
Thank you for your help!
The models are probably not the right layer. Indeed, you can make custom lookups [Django-doc] and custom model fields [Django-doc] to make the ORM language more "intuitive". To some extent, this is what Django does with for example a URLField
model field [Django-doc]: behind the curtains, it is just a VARCHAR
with extra logic for it.
We can make a custom lookup for a URL field to extract the schema for example:
from django.db.models import CharField, Transform, URLField, Value
from django.db.models.functions import StrIndex, Substr
@URLField.register_lookup
class SchemaTransform(Transform):
lookup_name = 'schema'
output_field = models.CharField()
def as_sql(self, compiler, connection):
return Substr(
self.lhs, Value(1), StrIndex(self.lhs, Value(':')) - Value(1)
).as_sql(compiler, connection)
and for example filter with:
unsafe_urls = MyModel.objects.filter(url_field__schema='http')
That being said, I would put pragmatism before designing an app that aims to follow SOLID and GRASP principles completely: these should be more indicators that something is wrong: if it requires a lot of work to overcome modifying something small in the model, then clearly something is wrong with the software. But making software adhere 100% to the Open/Close principle typically only results in a lot of extra work that will almost never pay off in the end.