Недавно я прочёл книгу «Паттерны разработки на Python TDD, DDD и событийно-ориентированная архитектура». Основной акцент в ней сделан на том, как именно нужно структурировать программы, чтобы они по мере роста оставались простыми и удобными в поддержке. Это моя любимая тема из области программирования, поэтому, конечно же, книга мне понравилась. Пожалуй, я не возьмусь использовать именно те приёмы, которые авторы рекомендуют в книге — но они обсуждают классные идеи, напомнившие мне о задачах, встречавшихся в моей практике на предыдущих работах. Кроме того, отмечу, что английская версия книги есть в свободном доступе онлайн, так какие вообще вопросы?

В книге рассматривается предметно-ориентированное проектирование и событийно-ориентированная архитектура (в основу которой удобно закладывать микросервисы, но это не обязательно). В этом посте я немного раскрою наиболее понравившиеся мне идеи из книги, но, прежде, чем переходить к этому, хочу артикулировать некоторые детали:
Детали
Книга читалась относительно быстро — приятная ясная проза, хорошие короткие главы.
Книга не сделала меня настоящим экспертом в DDD, но это нормально1.
Авторы очень постарались не слишком топить за микросервисы, и я это ценю.
В конце каждой главы есть краткая таблица с перечислением достоинств и недостатков, плюс по-настоящему честно обсуждается, стоит ли применять именно в вашей работе ту технику, что описана в этой главе. Многие разделы «против» были выдержаны примерно в следующем тоне, и мне это понравилось:
«Мы как могли постарались подчеркнуть, что каждый паттерн имеет свою цену. За каждый уровень косвенности требуется платить усложнением и дублированием кода, и всё это, определённо, может запутать программистов, никогда ранее не сталкивавшихся с такими паттернами»
Ладно, перейдём к хорошему.
Объекты-значения
В книге рекомендуется концептуализировать ключевые бизнес-примитивы в виде «объектов-значений» и использовать для этого классы данных. Вот пример из главы 1:
@dataclass(frozen=True)
class OrderLine:
order_id: OrderReference
sku: ProductReference
quantity: Quantity
Я тоже рекомендую так делать. Здесь важно, что этот класс ничего не знает о базе данных/ORM – это самый простой класс данных, ссылающийся на другие простые классы данных. Вам, в самом деле, будет удобно писать приятные и лёгкие в тестировании чистые функции, оперирующие такими данными. В идеальном мире все ваши базовые примитивы должны выглядеть именно так.
Это подводит нас к следующей теме, которая тесно связана с рассмотренной выше:
Принцип инверсии зависимостей
Этого термина я ранее не знал. Явно это какая-то штука из объектно-ориентированного программирования (может быть, это D в SOLID?), и не сказать, что исходная формулировка мне сильно помогла, но авторы показывают на её основе одну технику, которую я нахожу очень привлекательной. Эту технику проще всего продемонстрировать в сравнении с предыдущим примером.
В большинстве баз кода, с которыми мне доводилось работать, ключевые примитивы были представлены не в виде объектов-значений, а при помощи вот таких моделей:
class OrderLine(ORMBaseClass):
order_id = orm.ForeignKey(Order)
sku = orm.ForeignKey(Product)
quantity = orm.IntegerField()
В подобной системе абсолютное большинство кода работает непосредственно с такими моделями, в которых делается акцент на обращении к базам данных, из-за чего становится гораздо сложнее надёжно писать чистые функции. Вместо этого обычно получается код, переполненный мелкими операциями «чтение+запись» в базе данных. Код, написанный таким образом, плохо поддаётся модульному тестированию (поскольку приходится пропатчивать все эти взаимодействия с базой данных) и обычно быстро усложняется, так как в нём приходится поддерживать всё больше мелких чтений и записей. Ведь когда этих операций уже так много, разве повредит еще одна?
Авторы советуют выбирать в качестве базовых примитивов вашей системы не такие модели, сосредоточенные на работе с базами данных, а простые, написанные на чистом Python, например, такой замороженный класс данных, который рассмотрен выше. При этом, модели ваших баз данных должны производиться именно от этих моделей на чистом Python. Иными словами:
ORM импортирует (то есть «зависит от» или «знает») модель предметной области, а не наоборот.
Я хотел бы когда-нибудь поработать с системой, которая устроена именно так :)
Чистые функции
В книге то и дело упоминаются чистые функции2, хотя, я не помню, чтобы авторы где-то достаточно подробно разбирали эту тему «в лоб». Это нормально, поскольку в книге проделана большая работа по демонстрации этих функций в действии.
Например, в главе 3 подробно рассказано о программе для синхронизации файлов между двумя каталогами, и авторы пытаются обрисовать, как упростить тестирование всего этого. Сначала вся программа занимается операциями непосредственно над файловой системой, поэтому при всех тестах авторам приходится поднимать какие-то временные каталоги, записывать в каждый из них набор файлов, вызывать программу, а затем проверять, что она сделала во временных каталогах. Ик!
Затем они предлагают иной подход: «отделим, что мы хотим сделать от того, как хотим это сделать». Они меняют ядро своей программы так, что она принимает на вход два словаря, и каждый из этих словарей соответствует набору файлов, содержащихся в каталоге:
source_files = {'hash1': 'path1', 'hash2': 'path2'}
dest_files = {'hash1': 'path1', 'hash2': 'pathX'}
В дальнейшем они дописывают программу так, что она возвращает список операций, которые собирается выполнить для синхронизации двух каталогов:
("COPY", "sourcepath", "destpath"),
("MOVE", "old", "new"),
Чтобы протестировать алгоритм синхронизации, авторам не приходится ни что-либо считывать из файловой системы, ни что-либо в неё записывать. Они могут просто передать в свою программу пару словарей, проверить данные, возвращаемые в выводе, и на основе этого проверять, собирается ли программа делать то, что нужно. Используемые в их тестах словари и кортежи составляются без труда, не дают никаких побочных эффектов, не требуют ни мокинга, ни патчей.
На периферии их программы есть и такой код, который а) проверяет файловую систему, создавая эти словари, которые потом будут подаваться на ввод и 2) вносит в файловую систему изменения, исходя из инструкций, содержащихся в этих командах, полученных на вывод — но это просто неизбежный факт. В данном случае действительно важно, что основная часть программы теперь не содержит побочных эффектов. Мило!
Такой подход часто называют «функциональное ядро, императивная оболочка»3, идея его заключается в том, что основная часть программы должна состоять из чистых функций, а на её периферии должны тонким слоем располагаться интерфейсы, которые и взаимодействуют с внешним миром. Мне эта идея очень нравится.
Заключение
Эта (ссылка на питер ком) книга вышла очень достойной. Я не собираюсь писать событийно-ориентированную систему на основе микросервисов, в которой активно применялись бы техники DDD, но мне было интересно почитать, что авторы рассказывают на все эти темы. Мне также понравилось, как они разобрали обозначенные выше идеи!
Моя настольная книга — “Domain Modeling Made Functional” Скотта Влашина, и, надеюсь, именно эта книга наконец поможет мне составить впечатление о DDD. Мне нравятся его выступления на YouTube, то и дело к ним возвращаюсь и пересматриваю все подряд. Отличный парень. ↩
Подробнее о чистых функциях: мне нравится лекция “Hoist Your IO”, а также это упражнение в рефакторинге вдобавок к нему. Вот, кстати, очень хороший пост! ↩
О, боже, только что обнаружил, что прямо пока я писал этот пост, Скотт Влашин выступил с лекцией ровно на эту же тему! Обязательно собираюсь посмотреть! ↩
Осталось всего 200 экземпляров! Не упустите шанс приобрести книгу «Паттерны разработки на Python TDD, DDD и событийно-ориентированная архитектура» до того, как она закончится на нашем сайте piter.com