Python. DTO + аннотации типов

Alexei Stoliarov
4 min readJan 25, 2019

--

Python — отличный язык с помощью которого достаточно легко и быстро можно разрабатывать.Однако ингода динамическая типизация Python позволяет писать код, который просто писать, но довольно сложно читать и, как следствие, поддерживать.

В этой статье я хочу рассмотреть паттерн Data Transfer Object (DTO) который зачастую помогает сделать код более читабельным.

В начале был Dict

Dict — это одна из самых часто-используемых структур в Python. Она хорошо отпимизированна и отлично подходит для передачи неструктурированных данных.

Хороший пример использования dict

В примере выше dict это часть API функции render. Действительно функция render это общая функция фреймворка и она ожидает сущность с переменным набором ключей/аттрибутов и возможностью итерироваться по ним.

Проблемы начинаются в тот момент, когда мы начинаем оперировать dict’ами (т.е. данными без явного указания схемы) там, где есть схема.

пример неочевидного кода с dict

Это пример плохого кода (Не потому, что в нем опущены обработки ошибок. Это сделано просто, чтобы не загромождать код) т.к. в нем по всему пайплайну обработки сущности Order схема данных нигде явно не указывается. Для того, чтобы понять какие данные вообще есть в этом dict’е надо сделать запрос в API и посмотреть, что же там находится.

Время для комментариев

Первое, что приходит в голову, чтобы улучшить читаемость — это добавить комментарий в функцию get_order для того, чтобы описать схему.

Пример метода get_order с комментариями

У этого подхода 2 проблемы:

  1. Когда код будет изменяться от разработчика потребуются дополнительные усилия, чтобы описать схему. Комментарий ведь можно и не исправлять(особенно если production падает и ты срочно его чинишь), а код будет работать.
  2. Если схема данных изменится, то код сломается не в том месте, где мы получили данные от сервиса, а в том месте, где мы к ним обращаемся. Т.е. если из ответа пропадет поле created_at, то код сломается в методе save_order.

Давайте укажем схему явно

Это довольно простая идея: давайте в функции get_order прежде чем возвращать полученные данные собирать из них новый dict. Таким образом мы просто явно укажем набор полей, которые будут возвращены и решим обе проблемы предыдущего подхода:

  1. Невозможно добавить новое поле в ответ функции не указав его явно (или не сломав наш паттерн сделав update у dict)
  2. Если схема ответа API изменится, то код сломается прямо в этом месте и мы сразу поймем что именно пошло не так.

Таким образом мы уже можем выяснить схему данных не выполняя запрос.

Но может быть можно еще улучшить читаемость?

Аннотации типов и Data Transfer Object

Лично я считаю, что поддержка аннотации типов (type annotations и Mypy) это лучший аргумент для перехода на Python3. Да, они требуют дополнительной поддержки, но они сильно улучшают читаемость и простоту поддержки кода (ведь часть проверок за вас сделает статический анализатор кода). Если вы не знакомы с аннотацией типов в Python рекомендую к прочтению статью Real Python на эту тему.

Окей, с аннотациями понятно, но что такое Data transfer object?

Data transfer object (DTO) — это объект предназначенный только для транспортировки данных т.е. в нем нет никакой логики (никаких методов кроме “магических”). У этого паттерна есть много областей применения (они хорошо описаны у Мартина Фаулера), но т.к. в Python явная типизация опциональна, то DTO в Python приобретают новое назначение: DTO помогает явно указать контракт между компонетами.

Наш обновленный пример:

Теперь мы обращаясь к любой функции в пайплайне обработки заказа видим с каким типом мы работаем, можем перейти к нему и понять какой формат данных внутри и какого они типа. Если вы еще добавите к этому Mypy вы снимете с себя обязанность следить за тем, чтобы работа с данными велась по правилам — в случае чего Mypy укажет на проблему (только валидируйте входящие через API данные, я в примере эту секцию опустил просто, чтобы не загромождать).

Какие плюсы мы получили по сравнению с предыдущим решением, когда взяли DTO:

  1. Теперь нам не надо переходить к методу (или запускать pdb/repl), который породил dict, что понять что внутри. С какого бы мы места не смотрели на код, если он работает с DTO мы всегда можем понять его структуру и типы.
  2. Просто добавив аннотацию типов и DTO мы ввели явный контракт между двумя частями нашей программы и можем изменять их независимо друг от друга.

Т.е. теперь мне всегда использовать DTO?

Нет. В начале статьи я приводил пример с функцией render из Django, где использование DTO скорее доставит неудобства: метод render (как метод фреймворка) должен оперировать с коллекцией в которой есть набор ключей и значений для них и dict там отлично подходит.

И в целом dict’ом достаточно удобно пользоваться. Так, что DTO точно не серебрянная пуля.

Однако аннотация типов + DTO хорошо помогают явно указать схему данных которые передвигаются внутри вашего приложения.

Так-же хороший паттерн использования DTO это его использование как границы двух компонентов, которые будут изменяться по разным причинам: например между бизнес-логикой и отображением или между бизнес-логикой и внешним API. Таким образом вы делаете проще изменение внутренностей этих модулей.

Лайфхак, как сделать код еще более читабельным с помощью DTO (но не всем это подходит)

Просто сделайте его иммутабельным (например сделав его на основе NamedTuple из модуля typing): таким образом, вы будете уверены, что если кто-то захочет изменить DTO — он создаст новый. Этот подход часто заставляет писать более явный код.

Пример с иммутабельным DTO:

Теперь внутри функции order_processing_task явно видно, что функция process_order изменяет order_info.

Конечно, такой код будет производить больше мусора за счет создания объектов на каждое изменение, однако если для вас это не критично, то это отличный способ улучшить читаемость.

А в чем разница между Entity из Clean Architecture и DTO?

Кто-то может заметить, что DTO сам по себе похож на сущности в слое Entity из Clean Architecture. Да, они похожи, но между ними есть разница.

Entity из Clean Architecture это сущность бизес-логики в которой может быть поведение (а лучше, чтобы и было). Правда поведение в entity должно оперировать только сущностями с того-же слоя. Как пример: Entity Cart должно уметь пересчитывать свою сумму в зависимотси от Entity Item которые лежат внутри нее.

Слой Entity из clean architecture — это тема для отдельной статьи и наверное я ее когда-нибудь напишу (но это не точно).

Производительность

Конечно, любой из описанных подходов надо выбирать исходя из требований вашей системы к чиаемости/потреблению памяти/скорости работы. Но мое мнение такое: только высоконагруженное приложение почувствует разницу между использованием dict и DTO.

При иммутабельных DTO все конечно уже не так: на каждое изменение данных придется выделять память на новый объект.

Резюме

DTO, как я уже писал выше, не серебрянная пуля, однако может значительно повысить читаемость и упростить контроль за изменениями Python кода.

--

--