Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save david-shiko/27d577d7677d917111a7ba7d1170a8aa to your computer and use it in GitHub Desktop.
Save david-shiko/27d577d7677d917111a7ba7d1170a8aa to your computer and use it in GitHub Desktop.
Python паттерн "Рпозиторий"
Ссылка на книгу: https://t.me/physics_lib/9153
Паттерн «Репозиторий»
Пришло время выполнить обещание использовать принцип инверсии
зависимостей как способ устранения связанности ключевой логики от
инфраструктурных обязанностей.
Мы представим паттерн «Репозиторий», упрощающую абстракцию хранения данных, которая позволяет устранить связанность слоя модели
и слоя данных. Приведем конкретный пример того, как эта упрощающая
абстракция делает систему более легкой в тестировании, скрывая сложности базы данных.
На рис. 2.1 дан предварительный обзор того, что мы собираемся создать:
объект Repository, который находится между моделью предметной области
и базой данных.
Предметная Репозитории
область
До
Предметная
область
После
реализация
Абстрактный
репозиторий
Репозиторий
SQLAlchemy
БД БД
Рис. 2.1. Вид модели до и после применения паттерна «Репозиторий»
54 Часть I. Создание архитектуры для поддержки моделирования предметной области
Код для этой главы находится в ветке chapter_02_repository на GitHub1
.
git clone https://github.com/cosmicpython/code.git
cd code
git checkout chapter_02_repository
# или, если пишете код по ходу чтения, возьмите за основу
# материал из предыдущей главы:
git checkout chapter_01_domain_model
Организация постоянного хранения модели
предметной области
В главе 1 мы создали простую модель предметной области, которая может
размещать заказы в партиях товара. Легко писать тесты для этого кода,
потому что не требуется никакой настройки зависимостей или инфраструктуры. Если бы нужно было управлять базой данных или обращаться
к API и создавать тестовые данные, то писать и поддерживать тесты было
бы труднее.
К сожалению, в какой-то момент придется отдать нашу крохотную совершенную модель в руки пользователей и бороться с реальным миром
электронных таблиц, веб-браузеров и условий гонки. В последующих
главах мы рассмотрим способы соединения идеализированной модели
предметной области с внешним состоянием.
Мы собираемся работать по принципам Agile, поэтому приоритет состоит
в том, чтобы как можно быстрее заполучить минимально жизнеспособный продукт. В нашем случае это будет веб-API. В реальном проекте
вы, возможно, сразу же погрузитесь в работу с несколькими сквозными
тестами и начнете подключать веб-фреймворк, тестируя элементы кода
снаружи.
Но мы знаем — несмотря ни на что, нам понадобится какая-то форма постоянного хранения, а вы работаете с учебником, а не над проектом, поэтому
мы можем позволить себе потратить чуть больше времени на разработку
«снизу вверх» и начать думать о хранении и базах данных.
1 См. https://oreil.ly/6STDu
Глава 2. Паттерн «Репозиторий» 55
Немного псевдокода: что нам потребуется?
Когда мы создаем первую конечную точку API, то знаем, что у нас будет
некий код, который выглядит примерно так:
Как будет выглядеть первая конечная точка API
@flask.route.gubbins
def allocate_endpoint():
# извлечь товарную позицию заказа из запроса
line = OrderLine(request.params, ...)
# загрузить все партии товара из БД
batches = ...
# вызвать профильную службу
allocate(line, batches)
# затем каким-то образом сохранить размещение обратно в БД
return 201
Мы использовали веб-фреймворк Flask, потому что он легок в применении, но для того, чтобы понять эту книгу, вам не обязательно надо быть
пользователем Flask. На самом деле мы вам покажем, как можно сделать
выбор фреймворка не таким уж важным.
Нам понадобится способ, который позволяет извлекать информацию
о партиях товара из базы данных и создавать из нее экземпляры объектов
модели предметной области. А еще надо найти способ, который позволяет
сохранять их обратно в базу данных.
Что-что? А-а, gubbins в инструкции @flask.route.gubbins — это британский
вариант слова stuff («штуки», «фигня»).Не обращайте на это внимания.
Это псевдокод.
Применение принципа инверсии зависимостей
для доступа к данным
Как уже упоминалось во введении, подход на основе многослойной архитектуры широко распространен при структурировании системы, которая имеет
пользовательский интерфейс, некоторую логику и базу данных (рис. 2.2).
56 Часть I. Создание архитектуры для поддержки моделирования предметной области
Структура «модель — вид — шаблон» (Model-View-Template) фреймворка
Django тесно связана, как и «модель — вид — контролер» (Model-ViewController, MVC). В любом случае, цель состоит в том, чтобы держать
слои разделенными (что хорошо) и чтобы каждый слой зависел только
от расположенного ниже.
Слой базы данных
Бизнес-логика
Слой визуализации
Рис. 2.2. Многослойная архитектура
Но мы хотим, чтобы модель предметной области не имела никаких зависимостей1
. Мы не хотим, чтобы инфраструктурные обязанности растекались
по модели предметной области и замедляли юнит-тесты или внесение
изменений.
Вместо этого, как обсуждалось во введении, мы будем считать, что модель
находится «внутри» и зависимости втекают в нее; это то, что иногда называют луковой архитектурой (рис. 2.3).
Слой базы данных
Модель предметной области
Слой визуализации
Рис. 2.3. Луковая архитектура
1 Здесь я предполагаю, что нам не нужно «никаких зависимостей с поддержкой внутреннего состояния». Вполне нормально зависеть от вспомогательной библиотеки,
а вот зависеть от ORM или веб-фреймворков — нет.
Глава 2. Паттерн «Репозиторий» 57
ЭТО ПОРТЫ И АДАПТЕРЫ?
Если вы читали о паттернах проектирования, то, возможно, задаете себе такие
вопросы:
Это порты и адаптеры? Или это гексагональная архитектура? Разве это то же
самое, что и луковая архитектура? А как быть с чистой архитектурой? Что
такое порт и что такое адаптер? Народ, почему у вас так много слов для одного
и того же?
Хотя некоторые любят придираться к различиям, все это в значительной степени
означает одно и то же и сводится к принципу инверсии зависимостей: высокоуровневые модули (предметная область) не должны зависеть от низкоуровневых
(инфраструктура)1
.
Далее поговорим о некоторых тонкостях, связанных с «зависимостью от абстракций», и о том, существует ли питоновский эквивалент интерфейсов. См. также
раздел «Что такое порт и что такое адаптер в Python» на с. 70.
Напоминание: наша модель
Давайте вспомним, как выглядит наша модель предметной области
(рис. 2.4): размещение — это концепция связывания позиции заказа (OrderLine) с партией товара (Batch). Мы храним размещения в виде коллекции
на объекте Batch.
Рис. 2.4. Наша модель
Давайте посмотрим, как мы могли бы передать все это в реляционную
базу данных.
1 У Марка Симанна есть отличный пост на эту тему: https://oreil.ly/LpFS9.
58 Часть I. Создание архитектуры для поддержки моделирования предметной области
«Нормальный» способ ORM: модель зависит от отображения
В наши дни команды разработчиков практически не выполняют свои
SQL-запросы вручную. Для этого наверняка используется какой-либо
фреймворк для генерирования SQL на основе объектов модели.
Эти фреймворки называются объектно-реляционными отображениями
(object-relational mapper, ORM), поскольку они существуют для преодоления концептуального разрыва между миром объектов и моделирования
предметной области и миром баз данных и реляционной алгебры.
Самая важная вещь, которую дает нам объектно-реляционное отображение, — это неосведомленность об используемой системе постоянного
хранения: ее суть в том, что наша капризная модель предметной области
не должна ничего знать о способах загрузки или хранения данных. Это помогает держать модель независимой от конкретных технологий баз данных1
.
Но если вы будете следовать типичному туториалу по ORM SQLAlchemy,
то в итоге получите что-то вроде этого:
«Декларативный» синтаксис SQLAlchemy, модель зависит от ORM (orm.py)
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class Order(Base):
id = Column(Integer, primary_key=True)
class OrderLine(Base):
id = Column(Integer, primary_key=True)
sku = Column(String(250))
qty = Integer(String(250))
order_id = Column(Integer, ForeignKey('order.id'))
order = relationship(Order)
class Allocation(Base):
...
1 В этом смысле использование ORM уже является примером принципа инверсии
зависимостей. Вместо того чтобы зависеть от жестко запрограммированного SQL,
мы зависим от абстракции, ORM. Но в данной книге нам и этого мало!
Глава 2. Паттерн «Репозиторий» 59
Вам вовсе не нужно быть экспертом в SQLAlchemy, чтобы увидеть, что
первозданная модель теперь полна зависимостей от ORM и, кроме того,
начинает выглядеть адски скверно. Можем ли мы и вправду сказать, что
эта модель не осведомлена о базе данных? Каким образом она может быть
отделена от обязанностей по хранению данных, когда свойства модели
напрямую связаны со столбцами БД?
ORM ФРЕЙМВОРКА DJANGO, ПО СУЩЕСТВУ, ТАКОЕ ЖЕ,
НО ИМЕЕТ БОЛЕЕ ОГРАНИЧИТЕЛЬНЫЙ ХАРАКТЕР
Если вы привыкли к Django, то предыдущий «декларативный» фрагмент кода
SQLAlchemy переводится примерно вот так:
Пример объектно-реляционного отображения в Django
class Order(models.Model):
pass
class OrderLine(models.Model):
sku = models.CharField(max_length=255)
qty = models.IntegerField()
order = models.ForeignKey(Order)
class Allocation(models.Model):
...
Суть в том же — классы модели наследуют непосредственно от классов ORM,
поэтому модель зависит от него. Мы хотим, чтобы все было наоборот.
Django не предоставляет эквивалента классическому попарному ORM SQLAlchemy,
но примеры применения инверсии зависимостей и паттерна «Репозиторий»
к Django можно найти в приложении Г книги.
Инвертирование зависимости: ORM зависит от модели
К счастью, это не единственный способ использовать SQLAlchemy. Альтернативой является отдельное определение вашей схемы и определение
явного попарного отображения, задачей которого будет конвертирование
между схемой и моделью предметной области. В SQLAlchemy это называется классическим попарным отображением1
.
1 См. https://oreil.ly/ZucTG
60 Часть I. Создание архитектуры для поддержки моделирования предметной области
Явное ORM с участием объектов Table инструмента SQLAlchemy (orm.py)
from sqlalchemy.orm import mapper, relationship
import model Œ
metadata = MetaData()
order_lines = Table( 
'order_lines', metadata,
Column('id', Integer, primary_key=True, autoincrement=True),
Column('sku', String(255)),
Column('qty', Integer, nullable=False),
Column('orderid', String(255)),
)
...
def start_mappers():
lines_mapper = mapper(model.OrderLine, order_lines) Ž
Œ Объектно-реляционное отображение импортирует (или «зависит от»,
или «осведомлено о») модель предметной области, а не наоборот.
 Мы определяем таблицы и столбцы базы данных с помощью абстракций
SQLAlchemy1
.
Ž Когда мы вызываем функцию mapper, SQLAlchemy творит свои загадочные манипуляции, чтобы привязать классы модели предметной области
к различным таблицам, которые мы определили.
В итоге будет происходить следующее: если мы вызовем start_mappers,
то сможем легко загружать и сохранять экземпляры модели предметной
области из базы данных и в базу данных. Но если функция так и не будет
вызвана, то классы модели предметной области остаются в блаженном
неведении о базе данных.
Это дает нам все преимущества SQLAlchemy, включая возможность использовать alembic для миграции и делать запросы прозрачно с помощью
классов предметной области, в чем мы позже убедимся.
1 Даже в проектах, где мы не используем ORM, мы часто применяем SQLAlchemy наряду с легковесным инструментом миграции Alembic для декларативного создания
схем в Python и управления миграциями, соединениями и сеансами.
Глава 2. Паттерн «Репозиторий» 61
Когда вы впервые пытаетесь создать конфигурацию ORM, порой неплохо
писать для нее тесты, как в следующем примере:
Тестирование ORM напрямую (одноразовые тесты) (test_orm.py)
def test_orderline_mapper_can_load_lines(session): Œ
session.execute(
'INSERT INTO order_lines (orderid, sku, qty) VALUES '
'("order1", "RED-CHAIR", 12),'
'("order1", "RED-TABLE", 13),'
'("order2", "BLUE-LIPSTICK", 14)'
)
expected = [
model.OrderLine("order1", "RED-CHAIR", 12),
model.OrderLine("order1", "RED-TABLE", 13),
model.OrderLine("order2", "BLUE-LIPSTICK", 14),
]
assert session.query(model.OrderLine).all() == expected
def test_orderline_mapper_can_save_lines(session):
new_line = model.OrderLine("order1", "DECORATIVE-WIDGET", 12)
session.add(new_line)
session.commit()
rows = list(session.execute('SELECT orderid, sku, qty FROM
"order_lines"'))
assert rows == [("order1", "DECORATIVE-WIDGET", 12)]
Œ Если вы не знакомы с библиотекой pytest, то нам стоит объяснить использование аргумента session в этом тесте. Вообще-то для понимания
этой книги вам не нужно разбираться в деталях pytest или ее фикстурах.
Мы ограничимся лишь кратким объяснением. Вы можете определять общие зависимости для своих тестов как фикстуры (fixture) и pytest введет
их в тесты, где они нужны, посмотрев на их функциональные аргументы.
В данном случае это сеанс базы данных SQLAlchemy.
Скорее всего, эти тесты вам не пригодятся — как вы вскоре увидите, инвертировав зависимость от ORM и модели предметной области, останется
сделать лишь крохотный дополнительный шаг для реализации еще одной
абстракции — паттерна «Репозиторий», для которого будет легче писать
тесты и который позже обеспечит простой шаблон интерфейса для тестирования.
62 Часть I. Создание архитектуры для поддержки моделирования предметной области
Но мы уже достигли инверсии традиционной зависимости — модель предметной области остается «чистой» и свободной от инфраструктурных
обязанностей. Мы можем отказаться от SQLAlchemy и использовать другое
ORM или совершенно другую систему постоянного хранения, и модель
предметной области вообще не нуждается в изменении.
В зависимости от того, что вы делаете в своей модели предметной области,
и в особенности если вы отклоняетесь от объектно ориентированной парадигмы, вам будет труднее добиться нужного поведения ORM. Потребуется
модифицировать свою модель предметной области1
. Как это часто бывает
с архитектурными решениями, придется пойти на компромисс. Как гласит
Дзен Python, «практичность важнее безупречности»!
Но на данный момент конечная точка API может выглядеть примерно так,
и мы могли бы без проблем заставить ее работать:
Использование SQLAlchemy непосредственно в конечной точке API
@flask.route.gubbins
def allocate_endpoint():
session = start_session()
# извлечь товарную позицию заказа из запроса
line = OrderLine(
request.json['orderid'],
request.json['sku'],
request.json['qty'],
)
# загрузить все партии товара из БД
batches = session.query(Batch).all()
# вызвать службу предметной области
allocate(line, batches)
# сохранить размещение в базе данных
session.commit()
return 201
1 Привет удивительно полезным специалистам по сопровождению SQLAlchemy
и Майку Байеру в частности.
Глава 2. Паттерн «Репозиторий» 63
Введение паттерна «Репозиторий»
Паттерн «Репозиторий» — это абстракция поверх системы постоянного
хранения. Он скрывает скучные детали доступа к данным, делая вид, что
все данные находятся прямо в памяти.
Если бы в ноутбуках была бесконечная память, то в неуклюжих базах
данных не было бы нужды. Можно было бы просто использовать объекты
когда угодно. Как бы это выглядело?
Вы должны откуда-то получить свои данные
import all_my_data
def create_a_batch():
batch = Batch(...)
all_my_data.batches.add(batch)
def modify_a_batch(batch_id, new_quantity):
batch = all_my_data.batches.get(batch_id)
batch.change_initial_quantity(new_quantity)
Даже если объекты будут находиться прямо в памяти, мы должны их кудато поместить, чтобы найти снова. Данные в памяти позволят добавлять
новые объекты как список или множество. Поскольку объекты находятся
в памяти, нам никогда не придется вызывать метод .save(); мы просто извлекаем объект, который нас интересует, и модифицируем его в памяти.
Хранилище в абстрактном виде
Простейший репозиторий имеет всего два метода: add(), чтобы поместить
новый элемент в репозиторий, и get(), чтобы вернуть ранее добавленный
элемент1
. Мы твердо придерживаемся использования этих методов для доступа к данным в модели и в слое служб предметной области. Это условие
не позволяет нам прицепить модель предметной области к базе данных.
1 Вы можете спросить: «А как насчет операций выведения списка (list), удаления
(delete) или обновления (update)?» Однако в идеальном мире мы модифицируем
модельные объекты по одному за раз, и удаление обычно обрабатывается как мягкое
удаление — то есть batch.cancel(). Наконец, как вы увидите в главе 6, обновлением
занимается паттерн UoW.
64 Часть I. Создание архитектуры для поддержки моделирования предметной области
Вот как будет выглядеть абстрактный базовый класс (ABC) для репозитория:
Самый простой из возможных репозиториев (repository.py)
class AbstractRepository(abc.ABC):
@abc.abstractmethod Œ
def add(self, batch: model.Batch):
raise NotImplementedError 
@abc.abstractmethod
def get(self, reference) -> model.Batch:
raise NotImplementedError
Œ Совет: @abc.abstractmethod — это одна из немногих вещей, которая действительно заставляет абстрактные базовые классы «работать» в Python.
Язык не позволит вам создавать экземпляр класса, который не реализует все
абстрактные методы abstractmethod, заданные в его родительском классе1
.
 Можно использовать raise NotImplementedError, чтобы понять ошибку
отсутствующей реализации, но это не обязательно и недостаточно. На самом
деле ваши абстрактные методы могут иметь реальное поведение, которое
подклассы могут вызывать, если вам это и вправду нужно.
АБСТРАКТНЫЕ БАЗОВЫЕ КЛАССЫ, УТИНАЯ ТИПИЗАЦИЯ И ПРОТОКОЛЫ
Мы используем абстрактные базовые классы в этой книге по дидактическим причинам: мы надеемся, что они помогут объяснить, что такое интерфейс абстракции
репозитория.
В реальной жизни мы порой удаляем абстрактные базовые классы из производственного кода, потому что Python слишком легко их игнорирует и они в конечном
счете остаются без поддержки, а в худшем случае вводят в заблуждение. На практике в целях задействования абстракции мы часто просто опираемся на утиную
типизацию Python. Для питониста репозиторий — это любой объект, имеющий
методы add(thing) и get(id).
В качестве альтернативы можно рассмотреть протоколы PEP 5442
. Благодаря им
можно делать типизацию без возможности наследования, что особенно понравится
поклонникам идеи «композиция лучше наследования».
1 Для того чтобы по-настоящему извлечь пользу из абстрактных базовых классов
(какими бы они ни были), используйте помощников, таких как pylint и mypy. 2 См. https://oreil.ly/q9EPC
Глава 2. Паттерн «Репозиторий» 65
В чем компромисс?
Знаете, говорят, что экономисты знают цену всему и ценность ничего.
Скажем так, программисты знают выгоду от всего и ценность компромиссов.
Рич Хикки
Всякий раз, когда в этой книге мы вводим паттерн, мы всегда спрашиваем:
«А что мы за это получаем и чего это стоит?»
Как минимум мы вводим дополнительный слой абстракции. Хоть мы и надеемся, что это уменьшит сложность в целом, этот слой все же добавляет
сложность локально и имеет свою цену с точки зрения количества движущихся частей, из которых состоит вся система1
, а также объема текущего
технического сопровождения.
И все же паттерн «Репозиторий», вероятно, является одним из самых простых, если вы уже идете по пути DDD и инверсии зависимостей. Что касается кода, то на самом деле мы просто изымаем абстракцию SQLAlchemy
(session.query(Batch)), меняя ее на другую разработанную нами абстракцию (batches_repo.get).
Нам придется писать несколько строк кода в классе репозитория всякий
раз, когда мы добавляем новый объект модели предметной области, который хотим получить, но взамен получаем простую абстракцию над слоем
хранения, который мы контролируем. Паттерн «Репозиторий» позволит
легко вносить фундаментальные изменения в способ хранения (см. приложение В), и как мы увидим, его легко подделывать для юнит-тестов.
Кроме того, паттерн «Репозиторий» настолько распространен в мире предметно-ориентированного проектирования, что ваши коллеги, пришедшие
вPython из мира Java и C#, скорее всего, его узнают. Он приведен на рис. 2.5.
Как всегда, начинаем с теста. Указанный тест, вероятно, был бы классифицирован как интеграционный, так как мы выполняем проверку на
правильность интегрирования нашего кода (репозитория) с базой данных;
1 В англоязычной литературе по объектно ориентированному программированию для
обозначения частей, составляющих систему (работающие вместе серверы, конфигурации, фреймворки и т. д.), используется термин moving parts, по аналогии с деталями
автомобильного двигателя. — Примеч. ред.
66 Часть I. Создание архитектуры для поддержки моделирования предметной области
следовательно, тесты склонны смешивать сырой SQL с вызовами и подтверждениями истинности (инструкциями assert) в собственном коде.
Слой приложения
Слой базы данных
Репозиторий
Объекты модели
предметной области
Рис. 2.5. Паттерн «Репозиторий»
В отличие от предыдущих тестов ORM, эти тесты лучше надолго оставить в вашей кодовой базе, особенно если из-за каких-либо частей вашей
модели предметной области объектно-реляционное сопоставление будет
нетривиальным.
Тест репозитория на сохранение объекта (test_repository.py)
def test_repository_can_save_a_batch(session):
batch = model.Batch("batch1", "RUSTY-SOAPDISH", 100, eta=None)
repo = repository.SqlAlchemyRepository(session)
repo.add(batch) Œ
session.commit() 
rows = list(session.execute(
'SELECT reference, sku, _purchased_quantity, eta FROM "batches"' Ž
))
assert rows == [("batch1", "RUSTY-SOAPDISH", 100, None)]
Œ repo.add() — это тестируемый здесь метод.
 Мы держим .commit() за пределами репозитория и делаем операцию
фиксации обязанностью вызывающей части кода. В этом есть свои плюсы
и минусы; некоторые из наших доводов станут яснее в главе 6.
Ž Мы используем сырой SQL для проверки сохранения нужных данных.
Глава 2. Паттерн «Репозиторий» 67
Следующий тест предусматривает извлечение партий товара иразмещений,
поэтому он сложнее:
Тест репозитория на извлечение сложного объекта (test_repository.py)
def insert_order_line(session):
session.execute( Œ
'INSERT INTO order_lines (orderid, sku, qty)'
' VALUES ("order1", "GENERIC-SOFA", 12)'
)
[[orderline_id]] = session.execute(
'SELECT id FROM order_lines WHERE orderid=:orderid AND
sku=:sku',
dict(orderid="order1", sku="GENERIC-SOFA")
)
return orderline_id
def insert_batch(session, batch_id):  ...
def test_repository_can_retrieve_a_batch_with_allocations(session):
orderline_id = insert_order_line(session)
batch1_id = insert_batch(session, "batch1")
insert_batch(session, "batch2")
insert_allocation(session, orderline_id, batch1_id) Ž
repo = repository.SqlAlchemyRepository(session)
retrieved = repo.get("batch1")
expected = model.Batch("batch1", "GENERIC-SOFA", 100, eta=None)
assert retrieved == expected # Batch.__eq__ сравнивает только
ссылку Ž
assert retrieved.sku == expected.sku 
assert retrieved._purchased_quantity == expected._purchased_quantity
assert retrieved._allocations == { 
model.OrderLine("order1", "GENERIC-SOFA", 12),
}
Œ Здесь тестируется сторона чтения, поэтому сырой SQL готовит данные,
которые затем будут прочитаны методом repo.get().
 Избавим вас от подробностей методов insert_batch и insert_allocation;
их суть в том, чтобы создать несколько партий товара и для интересующей
нас партии выделить одну существующую строку заказа.
68 Часть I. Создание архитектуры для поддержки моделирования предметной области
Ž И это то, что мы здесь проверяем. Первая инструкция, assert ==, проверяет, совпадают ли типы и является ли ссылка одинаковой (потому что,
как вы помните, Batch — это сущность и у нас есть для нее собственный eq).
 Поэтому мы также явным образом проверяем его главные атрибуты,
в том числе ._allocations, который представляет собой питоновское множество объектов-значений OrderLine.
Независимо от того, старательно ли вы пишете тесты для каждой модели
или нет, это остается на ваше усмотрение. После того как вы протестировали
один класс на создание/модификацию/сохранение, вы с легким сердцем
можете продолжить в том же духе и протестировать другие минимальным
тестом «туда-обратно»1
или даже вообще ничего не делать, если все они
следуют аналогичной схеме. В нашем случае конфигурация ORM, которая
настраивает множество ._allocations, выглядит сложновато, поэтому заслуживает специального теста. В итоге получаем что-то вроде этого:
Типичный репозиторий (repository.py)
class SqlAlchemyRepository(AbstractRepository):
def __init__(self, session):
self.session = session
def add(self, batch):
self.session.add(batch)
def get(self, reference):
return self.session.query(model.Batch).filter_
by(reference=reference).one()
def list(self):
return self.session.query(model.Batch).all()
И теперь конечная точка Flask, возможно, будет выглядеть примерно так:
Использование репозитория непосредственно в конечной точке API
@flask.route.gubbins
def allocate_endpoint():
batches = SqlAlchemyRepository.list()
lines = [
1 Тест «туда-обратно» (round-trip test) — это тест, который взаимодействует только
через «входную дверь» (публичный интерфейс) тестируемой системы. — Примеч. пер.
Глава 2. Паттерн «Репозиторий» 69
OrderLine(l['orderid'], l['sku'], l['qty'])
for l in request.params...
]
allocate(lines, batches)
session.commit()
return 201
УПРАЖНЕНИЕ ДЛЯ ЧИТАТЕЛЯ
На днях мы столкнулись с другом на конференции по DDD. Он сказал, что не
использовал ORM уже десять лет. Паттерн «Репозиторий» и ORM действуют как
абстракции перед сырым SQL, поэтому применять один на фоне другого на самом
деле совсем не обязательно. Почему бы не попробовать реализовать репозиторий
без использования объектно-реляционного отображения? Код ищите на GitHub1
.
Мы оставили тесты репозитория, но выбор SQL зависит от вас. Может быть, это
будет труднее, чем вы думаете, а может быть, и легче. Но самое приятное, что
остальной части вашего приложения просто все равно.
Теперь поддельный репозиторий для тестов
создается просто!
Вот одна из самых больших выгод от паттерна «Репозиторий».
Простой поддельный репозиторий с использованием множества set (repository.py)
class FakeRepository(AbstractRepository):
def __init__(self, batches):
self._batches = set(batches)
def add(self, batch):
self._batches.add(batch)
def get(self, reference):
return next(b for b in self._batches if b.reference == reference)
def list(self):
return list(self._batches)
Поскольку он представляет собой простую оболочку вокруг структуры
данных set, все методы являются однострочными.
1 См. https://github.com/cosmicpython/code/tree/chapter_02_repository_exercise
70 Часть I. Создание архитектуры для поддержки моделирования предметной области
Использовать поддельный репозиторий в тестах и вправду несложно,
и у нас есть простая абстракция, которую просто использовать и о которой
легко рассуждать.
Пример использования поддельного репозитория (test_api.py)
fake_repo = FakeRepository([batch1, batch2, batch3])
Вы увидите эту подделку в действии в следующей главе.
Изготовление подделок для ваших абстракций — это отличный способ
оценить дизайн: если делать подделки трудно, то абстракция, вероятно,
является слишком сложной.
Что такое порт и что такое адаптер в Python
Не будем слишком подробно останавливаться на терминологии, потому
что главное, на чем стоит заострить внимание, — это инверсия зависимостей, а вот специфика используемого вами технического приема не имеет
большого значения. Кроме того, мы знаем, что разные люди используют
разные определения.
Порты и адаптеры пришли из объектно ориентированного мира, и мы придерживаемся определения, которое состоит в том, что порт — это интерфейс
между приложением и тем, что мы хотим абстрагировать, а адаптер — это
реализация, стоящая за этим интерфейсом или абстракцией.
Так вот, Python не имеет интерфейсов как таковых, поэтому, хотя адаптер
обычно легко идентифицировать, определить порт бывает сложнее. Если
вы используете абстрактный базовый класс, то это порт. Если нет, то порт —
это просто утиный тип, которому подчиняются ваши адаптеры и который
ваше стержневое приложение ожидает — имена используемых функций
и методов, а также имена и типы аргументов.
Если говорить конкретно, то в данной главе AbstractRepository — это порт,
а SqlAlchemyRepository и FakeRepository — адаптеры.
Глава 2. Паттерн «Репозиторий» 71
Выводы
Держа в голове цитату Рича Хикки, в каждой главе мы резюмируем издержки и выгоды каждого вводимого нами паттерна. Поймите нас правильно:
мы не говорим, что каждое отдельное приложение должно быть построено
именно таким образом; сложность приложения и предметной области
лишь иногда делает стоящим вложение времени и усилий в добавление
этих новых слоев косвенности.
В табл. 2.1 приведены некоторые плюсы и минусы паттерна «Репозиторий»
и модели, неосведомленной о системе постоянного хранения данных.
Таблица 2.1. Паттерн «Репозиторий» и неосведомленность о системе постоянного хранения:
компромиссы
Плюсы Минусы
y Есть простой интерфейс между системой постоянного
хранения и моделью предметной области.
y Легко изготовить поддельную версию репозитория для
юнит-тестирования или изъять разные решения по
хранению данных благодаря полному отделению модели
от особенностей инфраструктуры.
y Написание модели предметной области перед планированием обеспечения постоянства данных помогает
сосредоточиться на текущей бизнес-задаче. Если мы
когда-нибудь захотим радикально поменять подход, то
сможем сделать это в модели, не беспокоясь о внешних
ключах или миграциях вплоть до самого последнего
момента.
y Схема базы данных является очень простой, потому что
у нас есть полный контроль над тем, как мы сопоставляем объекты с таблицами
y Объектно-реляционное отображение (ОRМ)
частично устраняет связанность. Вполне возможно, что поменять внешние ключи будет
непросто, но при этом будет довольно легко
менять MySQL на Postgres и наоборот, если
вам когда-нибудь это понадобится.
y Поддержка объектно-реляционных сопоставлений вручную требует дополнительной
работы и дополнительного кода.
y Любой дополнительный слой косвенности
всегда увеличивает издержки на техническое
сопровождение и добавляет небезызвестный
фактор WTF для программистов на Python,
которые никогда раньше не сталкивались
с паттерном «Репозиторий»
На рис. 2.6 показан базисный тезис: да, для простых случаев работа над
отцепленной моделью предметной области дается сложнее, чем простой
паттерн «Объектно-реляционное отображение/активная запись»1
.
1 Указанный график вдохновлен статьей Роба Вэнса «Глобальная сложность, локальная
простота» (Global Complexity, Local Simplicity, см. https://oreil.ly/fQXkP) в его блоге.
72 Часть I. Создание архитектуры для поддержки моделирования предметной области
Но чем сложнее предметная область, тем больше окупятся усилия по освобождению от инфраструктурных вопросов, ведь тогда становится проще
вносить изменения.
Если ваше приложение представляет собой простую оболочку CRUD
(create-read-update-delete) вокруг базы данных, то модель предметной
области или репозиторий вам не нужны.
Сложность предметной области/бизнес-логики
Стоимость
изменений
Активная запись/
ORM
Модель предметной области
с паттерном «Репозиторий»
Рис. 2.6. График компромиссов модели предметной области
Приведенный пример кода недостаточно сложен, чтобы в полной мере показать, как выглядит правая сторона графика, но намеки есть. Представьте
себе, например, что однажды мы решим вносить изменения размещения
так, чтобы они располагались в объекте OrderLine, а не в объекте Batch: если
бы мы использовали, скажем, Django, то пришлось бы определять и продумывать миграцию базы данных, прежде чем мы могли бы выполнять
какие-либо тесты. Как бы то ни было, поскольку модель представлена
обыкновенными объектами Python, мы можем изменить set(), сделав его
новым атрибутом, и побеспокоиться о базе данных лишь в самом конце.
Вам интересно, как мы создаем экземпляры этих репозиториев, поддельных
или настоящих? Как на самом деле будет выглядеть приложение Flask?
Вы узнаете об этом в следующей захватывающей части, где рассказывается
о паттерне «Слой служб».
Но сначала небольшое отступление.
Глава 2. Паттерн «Репозиторий» 73
О ПАТТЕРНЕ «РЕПОЗИТОРИЙ»
Применяйте инверсию зависимостей к ORM
Модель предметной области должна быть свободной от вопросов инфраструктуры, поэтому ORM должно импортировать вашу модель, а не наоборот.
Паттерн «Репозиторий» — это простая абстракция вокруг системы постоянного
хранения данных
Репозиторий дает вам иллюзию коллекции объектов, находящихся в памяти.
Это позволяет легко создавать поддельный репозиторий FakeRepository для
тестирования и изымать фундаментальные детали вашей инфраструктуры,
не нарушая работу вашего стержневого приложения. См. пример в приложении В в конце книги.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment