[Python, Django] Архитектура в Django проектах — как выжить

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
01-Мар-2021 22:31

Думаю, ни для кого не секрет, что в разговорах опытных разработчиков Python, и не только, часто проскальзывают фразы о том, что Django это зло, что в Django плохая архитектура и на ней невозможно написать большой проект без боли. Часто даже средний Django проект сложно поддерживать и расширять. Предлагаю разобраться, почему так происходит и что с Django проектами не так.Немного теорииКогда мы начинаем изучать Django без опыта из других языков и фреймворков, помимо документации мы читаем туториалы, статьи, книги, и почти во всех видим что-то подобное:
Django — это фреймворк, использующий шаблон проектирования Model-View-Controller (MVC).
И дальше куча неточных схем и объяснений о том, что такое MVC. Почему они неточные и что с ними не так, можно посмотреть здесь или здесь. Обычно в таких схемах MVC описывают подобным образом:
  • Model — доступ к хранилищу данных
  • View — это интерфейс, с которым взаимодействует пользователь 
  • Controller — некий связывающий объект между model и view.
Данные распространенные схемы только запутывают и мешают, когда вы хотите написать приложение, в котором есть бизнес-логика.Стоит обратить внимание на две вещи.Первое, часто под M в MVC подразумевают — модель данных, и говорят, что это некий класс, который отвечает за предоставление доступа к базе данных. Что неверно, и не соответствует классическому MVC и его потомкам MV*. В классическом MVC под M подразумевается domain model — объектная модель домена, объединяющая данные и поведение. Если говорить точнее, то M в MVC это интерфейс к доменной модели, так как domain model это некий слой объектов, описывающий различные стороны определенной области бизнеса. Где одни объекты призваны имитировать элементы данных, которыми оперируют в этой области, а другие должны формализовать те или иные бизнес-правила.Второе, в  Django нет выделенного слоя controller, и когда вам говорят, что в Django слой views — это контроллер, не верьте этим людям. Обратитесь к официальной документации, а точнее к FAQ, тогда можно увидеть, что этот слой вписывается в принципы слоя View в MVC, особенно, если рассматривать DRF, а как такового слоя Controller в Django нет. Как говорится в FAQ, если вам очень хочется аббревиатур, то можно использовать в контексте Django аббревиатуру MTV (Model, Template, and View). Если очень хочется рассматривать Web MVC  и сравнивать Django с другими фреймворками, то для простоты можно считать view контроллером.Несмотря на то, что Django не соответствует MVC аббревиатуре, в ней реализуется главный смысл MVC  — отделение бизнес-логики от логики представления данных. Но на практике это не всегда так по нескольким причинам, которые мы рассмотрим ниже.Перейдем к практикеВыделим в Django приложениях несколько слоев, которые есть в каждом туториале и почти в каждом проекте:
  • front-end/templates
  • serializers/forms
  • views
  • models
Не будем рассматривать каждый слой подробно, это все можно найти в документации. В основном будем рассматривать Django  c использованием DRF.  Попробуем разобрать на двух простых кейсах, что стоит помещать в каждом из слоев и какая ответственность у каждого слоя. Первый кейс — создание заказа.  При создании заказа нам нужно:
  • проверить валидность заказа и доступность товаров
  • создать заказ
  • зарезервировать товар на складе
  • передать заявку менеджеру
  • оповестить пользователя о том, что его заказ принят в работу
Второй кейс — просмотр списка моих заказов. Здесь все просто, мы должны показать пользователю список его заказов:
  • получить список заказов пользователя 
Слой serializers/formsУ слоя serializers три основные функции (все выводы для serializers справедливы и для forms):
  • валидировать данные
  • преобразовывать данные запроса в типы данных Python
  • преобразовывать сложные Python объекты в простые типы данных Python (например, Django модели в dict)
Дополнительно сериалайзеры имеют два метода, create и update, которые вызываются в методе save() и почти всегда используются во view.Пример использования из документации:
class CommentSerializer(serializers.Serializer):
  email = serializers.EmailField()
  content = serializers.CharField(max_length=200)
  created = serializers.DateTimeField()
  def create(self, validated_data):
    return Comment.objects.create(**validated_data)
  def update(self, instance, validated_data):
    instance.email = validated_data.get('email', instance.email)
    instance.content = validated_data.get('content', instance.content)
    instance.created = validated_data.get('created', instance.created)
    instance.save()
    return instance
Где-то в нашей view:
# .save() will create a new instance.
serializer = CommentSerializer(data=data)
# .save() will update the existing `comment` instance.
serializer = CommentSerializer(comment, data=data)
comment = serializer.save()
В данном подходе за сохранение и обновление сущностей отвечает сериалайзер, точнее он оперирует методами модели.Можно использовать ModelSerializer и ModelViewSet, что позволяет писать CRUD методы в пару-тройку строк.
# serializers.py
class OrderSerializer(serializers.ModelSerializer):
  class Meta:
    model = Order
    fields = ‘__all__’
# views.py
class OrderViewsSet(viewsets.ModelViewSet):
  queryset = Order.objects.all()
  serializer_class = OrderSerializer
Если углубиться в реализацию ModelViewSet и ModelSerializer, то можно заметить, что за сохранение и обновление сущностей также отвечает сериалайзер. В таком случае, кажется, что переопределение create отличное место для того, чтобы описать там все бизнес-процессы и правила создания заказа.
# serializers.py
class OrderSerializer(serializers.ModelSerializer):
  class Meta:
    model = Order
    fields = []
  def create(self, validated_data):
    # Проверяем, что все товары есть и заказ валиден
    ...
    # Создаем запись о заказе в БД
    instance = super(OrderSerializer, self).create(validated_data)
    # Бронируем товары на складе
    ...
    # Передаем заявку менеджеру
    ...
    # Оповещаем пользователя
    ...
    return instance
# views.py
class OrderViewsSet(viewsets.ModelViewSet):
  queryset = Order.objects.all()
  serializer_class = OrderSerializer
Если с методом создания мы что-то придумали, то логику получения заказов пользователя придется помещать в view.Получается такая схема:
Плюсы данного подхода:Легко делать CRUDDjango и DRF предоставляют очень удобные инструменты с помощью которых можно легко создавать CRUD.Минусы данного подхода: Нарушение идей MVCМы смешиваем бизнес-логику с задачей сериализации (получения/отображения) данных в одном слое. Ни о каком выделенном слое бизнес-логики у нас нет и речи. Сериалайзеры стоит относить к слою View в MVC в контексте Django. Когда мы располагаем в сериалайзерах свою бизнес логику, мы нарушаем главный принцип  MVC — отделение логики представления данных от бизнес-логики.Нельзя переиспользоватьНе получится переиспользовать логику одного сериалайзера в другом сериалайзере, или в каком-то другом компоненте.Сложно тестироватьСложно протестировать бизнес-логику независимо от логики сериализации и валидации данных.Сложно поддерживатьОстается открытым вопрос — куда помещать методы на чтение данных, можно перенести их в слой views.Не все правила бизнес-логики можно поместить в сериалайзеры, иногда это излишне, потому что мы не используем сами сериалайзеры.В итоге мы получаем разбросанные бизнес-правила по всему проекту, которые невозможно переиспользовать и очень сложно поддерживать.Высокая зависимость от фреймворкаВысокая зависимость от DRF Serializers или Django Forms. Если мы захотим поменять способ сериализации и отказаться от serializers, то придется переносить или переписывать нашу логику. Также будет сложно переехать с Django Forms на DRF Serializers или наоборот.Правильные обязанности слояСериализация/десериализация данныхСериалайзер хорошо умеет сериализовывать данные, для этого его и нужно использовать.Валидация данныхБез написания кастомных валидаторов. Если вам требуется написать кастомный валидатор, то, скорее всего, это бизнес-правило, и данную проверку лучше вынести в слой с бизнес-логикой (где бы он ни был).ЗаключениеСериалайзеры точно не подходят для написания в них бизнес-логики. Если вам нужно что-то большее, чем CRUD, то стоит отказаться от использования метода save у сериалайзеров, так как сохранение данных не должно входить в их обязанности.Стоит отказаться от ModelSerializer с его магическими методами create и update и заменить их на обычные сериалайзеры (можно использовать ModelSerializer как read only — для удобства). Если вы пишете какое-то простое приложение, где кроме CRUD ничего не нужно, то можно не отказываться от удобства DRF и использовать сериалайзеры как предлагается в документации.Слой ViewsСлой View в контексте Django отвечает за представление и обработку пользовательских данных, в нем мы описываем, какие данные нам нужны и как мы хотим их представить. Если вспомнить то, о чем мы говорили в начале, то можно сразу сделать вывод, что во views не нужно писать бизнес-логику, иначе мы смешиваем логику представления данных с бизнес-логикой. Даже если считать, что views в Django это контроллеры, то размещать в них бизнес-логику тоже не стоит, иначе у вас получатся ТТУКи («Толстые, тупые, уродливые контроллеры»; Fat Stupid Ugly Controllers).Но часто можно увидеть что-то подобное:
# views.py
class OrderViewsSet(viewsets.ModelViewSet):
  queryset = Order.objects.all()
  serializer_class = OrderSerializer
  def perform_create(self, serializer):
    # Проверяем, что все товары есть и заказ валиден
    ...
    # Создаем запись о заказе в БД
    super(OrderViewsSet, self).perform_create(serializer)
    # Бронируем товары на складе
    ...
    # Передаем заявку менеджеру
    ...
    # Оповещаем пользователя
    ...
По дефолту, в ModelViewSet для сохранения и обновления данных используется сериалайзер, что не входит в его обязанности. Это можно исправить, полностью переопределить метод perform_create  (не вызывать super, но тогда встает вопрос об объективности наследования от ModelViewSet). Можно написать кастомные методы в ModelViewSet или написать кастомные APIView:
# views.py
class OrderCreateApi(views.APIView):
  class InputSerializer(serializers.ModelSerializer):
    number = serializers.IntegerField()
    ...
  def post(self, request):
    serializer = self.InputSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    # Проверяем, что все товары есть и заказ валиден
    ...
    # Создаем запись о заказе в БД
    order = Order.objects.create(**serializer.validated_data)
    # Бронируем товары на складе
    ...
    # Передаем заявку менеджеру
    ...
    # Оповещаем пользователя
    ...
    return Response(status=status.HTTP_201_CREATED)
В отличии от слоя serializers, мы теперь легко можем поместить нашу логику получения заказов в слой views.
# views.py
class OrderViewsSet(viewsets.ModelViewSet):
  queryset = Order.objects.all()
  serializer_class = OrderSerializer
  def get_queryset(self):
    queryset = super(OrderViewsSet, self).get_queryset()
    queryset = queryset.filter(user=self.request.user)
    return queryset
Тем самым, ограничив не только получение списка, но и другие методы CRUD, что, иногда, очень удобно и быстро. Также, можно переопределить каждый метод по отдельности.Получается такая схема:
Стоит помнить, что мы отказались от использования save у serializers. В таком случае слой serializers остается “чистым” и выполняет только “правильные” обязанности. Плюсы данного подходаЛегко делать CRUDDjango и DRF предоставляют очень удобные инструменты, с помощью которых можно легко создавать CRUD и views не исключение.Минусы данного подходаНарушение идей MVCМы смешиваем в одном слое логику представления данных и бизнес-логику приложения.Нельзя переиспользоватьНе получится переиспользовать логику одной view в другой view, или в каком-то другом компоненте. Так же будут проблемы, если мы захотим вызывать нашу бизнес-логику из других интерфейсов, например, из Celery задач.Высокая зависимость от фреймворкаВысокая зависимость от DRF View или Django View. Если мы захотим поменять способ обработки запроса и отказаться от views, то придется переносить или переписывать нашу логику. Будет сложно переехать с Django View на DRF View или наоборот.Сложно тестироватьДостаточно сложно протестировать код во views независимо от serializers и остальной инфраструктуры Django + придется использовать http client для тестирования.Сложно поддерживатьСо временем views разрастаются, часть логики переносится в Celery задачи, часть в модели, код во views дублируется, так как их нельзя переиспользовать — все это приводит к тому, что проект сложно поддерживать.Правильные обязанности слоя Обработка запросаВо view мы принимаем и обрабатываем запрос клиента, подготавливаем данные для передачи в бизнес-логику.Делегирование сериализации данных сериалайзерамВсю логику сериализации данных должны выполнять сериалайзеры.Вызов методов бизнес-логикиПосле подготовки и сериализации данных вызывается интерфейс бизнес-логики.Логика представления данныхМы должны обработать ответ от методов бизнес-логики и предоставить нужные данные клиенту.ЗаключениеВывод примерно такой же как и с serializers — в views не стоит размещать бизнес-логику.Стоит отказаться от ModelViewSet и миксинов, так как они используют сериалайзеры для сохранения данных, вместо этого использовать обычные APIView или GenericAPIView.Если вам нужен только CRUD, то можно использовать подход который предоставляет ModelViewSet и не усложнять себе жизнь.Слой models Если опираться на более продвинутые туториалы или вспомнить слой Model в MVC, то кажется, что models отличное место для размещения бизнес-логики.Это может выглядеть примерно так:
# models.py
class Order(models.Model):
  number = serializers.IntegerField()
  created = models.DateTimeField(auto_now_add=True)
  status = models.CharField(max_length=16)
  def update_status(self, status: str) -> None:
    self.status = status
    self.save(update_fields=('status',))
  ...
  @classmethod
  def create(cls, data...):
    instance = cls(...)
    # Проверяем, что все товары есть и заказ валиден
    ...
    # Создаем запись о заказе в БД
    instance = instance.save()
    # Бронируем товары на складе
    ...
    # Передаем заявку менеджеру (например на почту или создаем какую то запись в БД)
    ...
    # Оповещаем пользователя
    ...
# views.py
class OrderCreateApi(views.APIView):
  class InputSerializer(serializers.ModelSerializer):
    number = serializers.IntegerField()
    ...
  def post(self, request):
    serializer = self.InputSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    Order.create(**serializer.validated_data)
     return Response(status=status.HTTP_201_CREATED)
В view мы сериализуем данные с помощью serializer и вызываем метод создания заказа у класса модели. В данном случае мы реализовали classmethod, что бы не было необходимости создавать экземпляр модели. Иначе нам придется понимать какие данные относятся к полям модели, а какие мы должны передать в метод создания, а это уже некие бизнес- правила.Стоит заметить, что не нужно писать бизнес-логику в методе save(), так как это базовый метод модели и он может неоднократно использоваться в различных частях кода.Для методов получения данных в таком случае стоит использовать Managers.
# views.py
class OrderListApi(views.APIView):
  class OutputSerializer(serializers.ModelSerializer):
    class Meta:
      model = Order
      fields = ‘__all__’
  def get(self, request):
    orders = Order.objects.filter(user=request.user)
    # если у вас сложные условия фильтрации
    # например, Order.objects.filter(user=request.user, is_deleted=False, is_archived=False...)
    # то стоит написать кастомные методы в Manager
    data = self.OutputSerializer(orders, many=True).data
    return Response(data)
Получается такая схема:
В данном случае слои serializers и views становятся “чистыми” и правила бизнес-логики концентрируются в одном слое.Плюсы данного подходаСледование идеям MVCМы отделили логику представления данных от логики предметной области. View только подготавливает данные и вызывает методы модели, все бизнес правила и процессы описаны в методах модели. Данный подход соответствует главной идее MVC.Легко тестироватьВся бизнес-логика собрана в одном слое, который не зависит от других слоев, например, от views или serializers. Каждый метод модели можно протестировать по отдельности как обычный python код. Остается только замокать метод save и базовые методы managers или использовать базу данных, если требуется.Можно переиспользоватьМетоды модели можно вызывать из любого компонента, DRF Views, Django Views, Celery задачи и т.д.Минусы данного подходаЗависимость от фреймворкаУ нас все еще есть зависимость от фреймворка, но это не так критично. Так как отказ от Django models и ORM или их замена — очень редкий кейс.Сложно поддерживать большие проектыВ больших проектах много бизнес-правил и если все их описывать в одном классе модели, то модель разрастается и превращается в божественный объект, который сложно читать и поддерживать. Сложно масштабировать и разделять код по файлам, так как мы ограничены требованиями фреймворка. Непонятно, куда помещать методы, которые оперируют несколькими моделями, возможно, из разных модулей. Усложнение CRUD проектовЕсли вам нужен только CRUD, то данный подход увеличивает время разработки и не приносит плюсов.ЗаключениеМы не нарушили главных идей MVC и наш код соответствует им. Такой подход можно использовать в малых проектах, когда бизнес-логики не много и она умещается в классах моделей.Слой ServicesМы перебрали все дефолтные слои в Django приложении, теперь можем вспомнить о том, что под слоем Model в MVC подразумевается не один объект, а набор объектов. Выделим отдельный сервисный слой services внутри слоя Model, который будет отвечать за бизнес-правила предметной области и приложения. В models оставить только простые property, в которых нет сложных бизнес-правил, и методы для работы с собственными данными модели, например обновление полей. Тогда наши кейсы можно реализовать так:
# models.py
class Order(models.Model):
  number = serializers.IntegerField()
  created = models.DateTimeField(auto_now_add=True)
  status = models.CharField(max_length=16)
  def update_status(self, status: str) -> None:
    self.status = status
    self.save(update_fields=('status',))
  ...
# services.py
# вместо пречесления всех аргументов можно реализовать DTO
def order_create(name: str, number: int ...) -> bool:
  # Проверяем, что все товары есть и заказ валиден
  ...
  # Создаем запись о заказе в БД
  order = Order.objects.create(...)
  # Бронируем товары на складе
  ...
  # Передаем заявку менеджеру (например на почту или создаем какую то запись в БД)
  ...
  # Оповещаем пользователя
  ...
# views.py
class OrderCreateApi(views.APIView):
  class InputSerializer(serializers.ModelSerializer):
    number = serializers.IntegerField()
    ...
  def post(self, request):
    serializer = self.InputSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    services.order_create(**serializer.validated_data)
    return Response(status=status.HTTP_201_CREATED)
Стоит придерживаться следующему подходу:
  • views — подготовка данных запроса, вызов бизнес логики, подготовка ответа
  • serializers — сериализация данных, простая валидация
  • services — простые функции с бизнес правилами или классы (Service Objects)
  • managers — содержит в себе правила работы с данными (доступ к данным)
  • models — единственный окончательный источник правды о анных
Получение заказов пользователя:
# services.py
def order_get_by_user(user: User) -> Iterable[Order]:
  return Order.objects.filter(user=user)
# views.py
class OrderListApi(views.APIView):
  class OutputSerializer(serializers.ModelSerializer):
    class Meta:
      model = Order
      fields = ('id', 'number', ...)
  def get(self, request):
    orders = services.order_get_by_user(user=request.user)
    data = self.OutputSerializer(orders, many=True).data
    return Response(data)
Получается такая схема:
Плюсы данного подходаСледование идеям MVCКак и в предыдущем подходе, мы полностью отделили бизнес-логику от логики представления.Легко тестироватьСервисы представляют собой простые Python функции, которые легко тестировать.Можно переиспользоватьМы можем вызывать наши сервисы из любого компонента + можем повторно использовать какие-то сервисы в других проектах. Легко поддерживать и расширятьВ данном подходе выделен отдельный слой под бизнес-логику и логику приложения, при росте проекта сервисы можно декомпозировать и расширять. ГибкостьСуществует множество подходов написания и расширения сервисного слоя.Минусы данного подходаЗависимость от фреймворкаДоменный слой не отделен от слоя приложения и инфраструктуры. Мы все еще используем Django модели в качестве сущностей. У нас могут возникнуть проблемы, когда мы захотим отказаться от Django ORM, но это очень редкий кейс и для многих проектов неактуален.Усложнение CRUD проектовЕсли вам нужен только CRUD, то данный подход увеличивает время разработки и не приносит плюсов.ЗаключениеДанный подход удобно использовать в проектах различной сложности и размера. В проекте понятна структура, каждый слой и компонент имеет свою ответственность и не нарушает ее границы. Сервисы легко декомпозировать и отделять друг от друга, в более сложных случаях их можно объединять за фасадами.На самом деле, каким может быть сервисный слой и как его лучше выделять и разделять это тема отдельной статьи и даже книги, об этом много пишут Мартин Фаулер, Роберт Мартин и другие. Что касается Django, советую обратить внимание на стайл гайд от HackSoftware у них схожие взгляды, но они разделяют сервисный слой на два компонента (services и selectors) и не используют кастомные методы в managers. Подход написания serializers и включения их во views я взял у них. Также стоит посмотреть на идеи ребят из dry-python.Общий итогПолучается, что поддерживаемость и “чистота” Django проектов страдает от удобства и плюшек фреймворка. Django и DRF очень классные инструменты, но не все их возможности стоит использовать. Можно сделать вывод, что, чем больше ваш проект и чем сложнее в нем бизнес-правила и сущности, тем более абстрактным и независимым от фреймворка должен быть ваш код. И выделение сервисного слоя — это далеко не предел и не идеал архитектуры приложения.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_python, #_django, #_django, #_arhitektura (архитектура), #_python, #_blog_kompanii_vivid_money (
Блог компании Vivid Money
)
, #_python, #_django
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 07:16
Часовой пояс: UTC + 5