Как заставить взаимодействие Django Manager и Model следовать принципу Open/Closed?
Я разрабатываю модели для своего приложения Django App и озабочен тем, как отделить логику вызывающего пользователя от реализации модели, чтобы будущие изменения самой модели не требовали изменений в кодовой базе ниже по течению. Короче говоря, придерживаюсь принципа открытости/закрытости (OCP).
Мне интересно, как лучше всего это реализовать, используя при этом лучшие возможности фреймворка Django.
Концептуально, на мой взгляд, имеет смысл сделать следующее:
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
Пользовательский менеджер для логики запросов Для каждой модели определяется пользовательский менеджер, который отвечает за обработку всевозможной логики фильтрации модели. Например, вместо вызова
Foo.objects.filter(is_active=True)
, вызывающая сторона будет вызыватьFoo.objects.is_active()
. В будущем, если логика реализации is_active изменится, вызывающей стороне не нужно будет менять свою логику, сохраняя OCP.Инкапсуляция полей модели Для каждого поля модели определены
@property
геттеры и сеттеры, позволяющие изменять поля модели.Минимальная связь. Связь сохраняется только между Custom Manager и моделью.
Мои основные опасения по поводу такого подхода
Для пункта 1: Требование, чтобы вся логика фильтрации определялась как методы в пользовательском менеджере, потенциально может привести к десяткам методов в каждом менеджере (из-за комбинации полей), что затруднит управление.
По пункту 2:
Хотя это удобно при вызове атрибута модели, например Foo.bar
, базовое поле должно вызываться при использовании менеджера. Например, Foo.objects.filter(bar=x)
не работает, нужно использовать Foo.objects.filter(_bar=x)
. Это требование подрывает смысл пункта 2, если не реализован пункт 1.
Вопросы
Является ли этот подход хорошей практикой для достижения развязки в Django при соблюдении принципа открытости/закрытости?
Существуют ли в экосистеме Django более эффективные или более стандартные подходы для развязки логики модели и логики вызывающей стороны без излишней сложности?
Как опытные разработчики Django справляются с инкапсуляцией полей, гарантируя при этом, что запросы остаются интуитивно понятными и поддерживаемыми?
Спасибо за помощь!
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.
Мы можем сделать пользовательский поиск для поля URL, чтобы извлечь схему, например:
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)
и, например, отфильтровать с помощью:
unsafe_urls = MyModel.objects.filter(url_field__schema='http')
При этом я бы поставил прагматизм выше разработки приложения, которое стремится полностью следовать принципам SOLID и GRASP: они должны быть скорее индикаторами того, что что-то не так: если для преодоления модификации чего-то небольшого в модели требуется много работы, то явно что-то не так с программным обеспечением. Но если заставить программное обеспечение на 100% следовать принципу Open/Close, то это, как правило, приводит лишь к огромному количеству дополнительной работы, которая в итоге почти никогда не окупается.