2.Repository Pattern

 


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

Представляем вам шаблон Repository, он упрощает абстракцию над хранилищем данных, позволяющую нам отделить слой модели от слоя данных. Давайте приведем конкретный пример того, как эта упрощающая абстракция делает нашу систему более тестируемой, скрывая сложности базы данных.

Картина [maps_chapter_02] илюстрирует то, что мы собираемся построить: объект Repository, который находится между нашей моделью предметной области и базой данных.

images/apwp_0201.png
Figure 1. До и после шаблона репозитория
Tip

Код для этой главы находится в chapter_02_repository branch on GitHub.

git clone https://github.com/cosmicpython/code.git
cd code
git checkout chapter_02_repository
# или чтобы писать код вместе с нами, ознакомьтесь с предыдущей главой:
git checkout chapter_01_domain_model

Persisting Our Domain Model

В [chapter_01_domain_model] мы построили простую модель домена, которая может распределять заказы по партиям запасов. Мы относительно легко написали тесты для такого кода, потому что нет никаких зависимостей или инфраструктуры для настройки. Если бы нужно было запустить базу данных или API и создать тестовые данные, тесты было бы сложнее писать и поддерживать.

К сожалению, в какой-то момент нам придется передать эту идеальную маленькую модель в руки пользователей и бороться с реальным миром электронных таблиц, веб-браузеров и условий гонки. В следующих нескольких главах мы рассмотрим, как связать идеализированную модель предметной области с внешним состоянием.

В надежде на то, что мы будем работать гибко, наш основной приоритет — как можно быстрее получить минимально жизнеспособный продукт. В нашем случае это будет веб-API. В реальном проекте вы можете сразу погрузиться в несколько сквозных (end-to-end) тестов и начать подключать веб-фреймворк, тестируя функционал извне.

Но мы знаем, что, несмотря ни на что, нам понадобится какая-то форма постоянного хранения. Поскольку это учебник, мы можем позволить себе немного больше развития снизу вверх и начать думать о хранении и базах данных.

Псевдокод: Что делать то будем?

Когда мы создаём наш первый endpoint API, подразумеваем, что у нас будет некоторый код, который выглядит более или менее похожим на этот.

Example 1. Как будет выглядеть наш первый endpoint API
NoteМы использовали Flask, потому что он достаточно простой, но вам не нужно быть с Flask на "ты", чтобы понять эту книгу. На самом деле, наша задача объяснить, как сделать выбор фреймворка незначительной деталью.

Нам понадобится способ извлечения пакетной информации из базы данных и создания из нее экземпляров объектов модели домена, а также способ сохранения их обратно в базу данных.

Какого…? Ух-х-х, «gubbins» - это британское слово, означающее «фигня». Вы можете просто забить на это. Это’ж псевдокод, понятно?

Применение DIP для доступа к данным

Как уже упоминалось в введение, многоуровневая архитектура — это общий подход к структурированию системы, которая имеет пользовательский интерфейс, некоторую логику и базу данных (см. [layered_architecture2]).

images/apwp_0202.png
Figure 2. Многослойная архитектура

Структура Django Model-View-Template тесно связана, как и Model-View-Controller (MVC). В любом случае цель состоит в том, чтобы слои были разделены (что хорошо), и чтобы каждый слой зависел только от того, который находится под ним.

Надо, чтобы в нашей модели предметной области не было никаких зависимостей .[1] Не надо, чтобы проблемы с инфраструктурой проникли в нашу модель предметной области и замедлили наши модульные тесты или нашу способность вносить изменения.

Вместо этого, как обсуждалось во введении, мы будем думать, что наша модель находится "inside (внутри)", и зависимости текут внутрь неё; это то, что умные люди иногда называют onion (луковая) architecture (см. [onion_architecture]).

images/apwp_0203.png
Figure 3. Onion architecture
Это Порты и Адаптеры?

Если вы читали об архитектурных паттернах, вы можете задавать себе такие вопросы:

Это порты и адаптеры? Или это гексогональная архитектура? Это то же самое, что и луковая архитектура? А как насчет чистой архитектуры? Что такое порт и что такое адаптер? Почему у вас, "яйцеголовых", так много слов для одного и того же?

Хотя некоторые умники любят придираться к различиям, все это в значительной степени названия одного и того же, и все они сводятся к принципу инверсии зависимостей: модули высокого уровня (домен) не должны зависеть от модулей низкого уровня (инфраструктура).[2]

Мы рассмотрим некоторые мелочи, касающиеся «зависимости от абстракций», и того, существует ли Pythonic-эквивалент интерфейсов, later in the book. Смотрите также [what_is_a_port_and_what_is_an_adapter].

Напоминание: Наша модель

Давайте вспомним нашу модель предметной области (см. [model_diagram_reminder]): Распределение - это концепция связывания OrderLine с Batch. Мы сохраняем выделенные позиици как коллекцию в нашем объекте Batch.

images/apwp_0103.png
Figure 4. Наша модель

Давайте посмотрим, как мы можем перенести это в реляционную базу данных.

"Нормальный" способ это ORM: Модель зависит от ORM

В наши дни маловероятно, что члены вашей команды вручную создают свои собственные SQL-запросы. Вместо этого вы почти наверняка используете какой-то фреймворк для генерации строк SQL на основе ваших объектов модели.

Эти структуры называются объектно-реляционными картографами object-relational mappers (ОРМ), поскольку они существуют для преодоления концептуального разрыва между миром объектов и моделирования предметной области и миром баз данных и реляционной алгебры.

Самая важная вещь, которую дает нам ORM, - это игнорирование сохраняемости persistence ignorance: идея в том, что наша доменная модель не должна ничего знать о том, как данные загружаются или сохраняются. Это помогает сохранить наш домен чистым от прямых зависимостей конкретных технологий баз данных.[3]

Но если вы будете следовать типичному учебнику SQLAlchemy, то в итоге получите что-то вроде этого:

Example 2. SQLAlchemy "декларативный" синтаксис, модель зависит от ORM (orm.py)

Вам не нужно разбираться в SQLAlchemy, чтобы увидеть, что наша изначальная модель теперь полна зависимостей от ORM и к тому же начинает выглядеть чертовски уродливо. Можно ли сказать, что эта модель игнорирует базу данных? Как это можно отделить от проблем с хранением, когда свойства нашей модели напрямую связаны со столбцами базы данных?

ORM Django, по сути, тот же, но более строгий

Если вы больше привыкли к Django, предыдущий «декларативный» фрагмент SQLAlchemy можно перевести примерно так:

Example 3. Django ORM пример

Дело в том же - наши классы моделей наследуются напрямую от классов ORM, поэтому наша модель зависит от ORM. Мы хотим, чтобы все было наоборот.

Django не предоставляет эквивалента классическому мапперу SQLAlchemy, но примеры применения инверсии зависимостей и шаблона репозитория к Django см. в разделе [appendix_django].

Инвертирование зависимости: ORM зависит от модели

К счастью, это не единственный способ использовать SQLAlchemy. Альтернативой является определение вашей схемы отдельно и определение явного mapper-а для преобразования между схемой и нашей моделью предметной области, что SQLAlchemy называет classical mapping:

Example 4. Явное сопоставление ORM с объектами таблицы SQLAlchemy (orm.py)
from sqlalchemy.orm import mapper, relationship

import model  #1


metadata = MetaData()

order_lines = Table(  #2
    '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)  #3
1ORM импортирует (или "зависит от" или "знает о") модель предметной области, а не наоборот.
2Мы определяем таблицы и столбцы нашей базы данных с помощью абстракций SQLAlchemy.[4]
3Когда мы вызываем функцию mapper, SQLAlchemy творит чудеса, связывая классы нашей модели предметной области с различными таблицами, которые мы определили.

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

Это дает нам все преимущества SQLAlchemy, включая возможность использовать alembic для миграций и возможность прозрачного запроса с использованием наших классов домена, как мы увидим.

Когда вы впервые пытаетесь создать свою конфигурацию ORM, может быть полезно написать для неё тесты, как в следующем примере:

Example 5. Тестирование ОРМ напрямую (одноразовые тесты) (test_orm.py)
def test_orderline_mapper_can_load_lines(session):  #1
    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)]
1Если вы не использовали pytest, то аргумент session для этого теста нуждается в объяснении. Смысл такой: Вам не нужно беспокоиться о деталях pytest или его фикстурах в целях этой книги, но главная мысль состоит в том, что вы можете определить общие зависимости для ваших тестов в виде "fixtures", и pytest передаст их в тесты, которые нуждаются в них, приняв их в качестве аргументов функций. В данном случае это сеанс session базы данных SQLAlchemy.

Вероятно, вам не стоит хранить эти тесты. Как вы вскоре увидите, после того, как поближе познакомитесь с инверсией зависимости ORM и модели предметной области, это всего лишь небольшой дополнительный шаг для реализации другой абстракции, называемой шаблоном репозитория, для которого будет легче писать тесты, и он предоставит простой интерфейс для, скажем так — фейка, позже в тестах.

Но мы уже достигли нашей цели инвертировать традиционную зависимость: модель предметной области остается «чистой» и свободной от инфраструктурных проблем. Мы могли бы выбросить SQLAlchemy и использовать другую ORM или совершенно другую систему сохранения, и модель предметной области вообще не нуждалась бы в изменении.

В зависимости от того, что вы делаете в своей модели предметной области, и особенно если вы отходите далеко от парадигмы объектно-ориентированного программирования, вам может оказаться все труднее заставить ORM обеспечить точное поведение, которое вам нужно, и вам может потребоваться изменить модель предметной области. [5] Как это часто бывает с архитектурными решениями, вам нужно будет найти компромисс. Как говорит дзэн Python: «Практичность лучше чистоты!»

На данный момент, однако, наш endpoint API может выглядеть примерно так, и мы могли бы заставить её работать просто отлично:

Example 6. Использование SQLAlchemy непосредственно в нашем endpoint API

Знакомство с шаблоном репозитория

Шаблон Repository — это абстракция над постоянным хранилищем. Он скрывает скучные детали доступа к данным, делая вид, что все наши данные находятся в памяти.

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

Example 7. Вы должны откуда-то брать данные

Несмотря на то, что наши объекты находятся в памяти, нам нужно поместить их где-нибудь, чтобы снова найти их. Наши данные в памяти позволят нам добавлять новые объекты, как список или множество. Поскольку объекты находятся в памяти, нам никогда не нужно вызывать метод .save (); мы просто получаем объект, который нам нужен, и модифицируем его в памяти.

The Repository in the Abstract

В простейшем репозитории всего два метода: add () для добавления нового элемента в репозиторий и get() для возврата ранее добавленного элемента.[6]

Мы твердо придерживаемся использования этих методов для доступа к данным в нашем домене и на уровне сервиса. Эта добровольная простота не позволяет нам связать нашу модель предметной области с базой данных.

Вот как будет выглядеть абстрактный базовый класс (ABC) для нашего репозитория:

Example 8. Самый простой из возможных репозиториев (repository.py)
class AbstractRepository(abc.ABC):

    @abc.abstractmethod  #1
    def add(self, batch: model.Batch):
        raise NotImplementedError  #2

    @abc.abstractmethod
    def get(self, reference) -> model.Batch:
        raise NotImplementedError
1Python tip: @abc.abstractmethod — это одна из немногих вещей, которая заставляет ABCs действительно "работать" в Python. Python не позволит вам создать экземпляр класса, который не реализует все "абстрактные методы", определенные в его родительском классе.[7]
2raise NotImplementedError — это хорошо, но это не обязательно и не достаточно. На самом деле, ваши абстрактные методы могут иметь реальное поведение, которое подклассы могут вызвать, если вы действительно хотите.
Абстрактные базовые классы, утиная типизация и протоколы

Мы используем абстрактные базовые классы в этой книге по дидактическим соображениям: мы надеемся, что они помогут объяснить, что такое интерфейс абстракции репозитория.

В реальной жизни мы иногда обнаруживаем, что удаляем ABC из нашего продакшен кода, потому что Python слишком упрощает их игнорирование, и они в конечном итоге не обслуживаются и, в худшем случае, вводят в заблуждение. На практике мы часто просто полагаемся на утиную типизацию Python для включения абстракций. Для Pythonista репозиторий — это любой объект, имеющий add(thing) and get(id) methods.

Альтернативой для изучения является PEP 544 protocols. Это дает вам возможность писать классы без возможного использования наследования, что особенно понравится фанатам "предпочитать композицию наследованию".

Что такое компромисс?

Знаете, говорят, что экономисты знают всё о цене и ничего о ценности? Программисты же, знают всё о преимуществе и ничего о компромисе.

— Рич Хикки

Всякий раз, когда мы представляем архитектурный паттерн в этой книге, мы всегда задаёмся вопроосом: «Что нам ЭТО даст? И во что нам ЭТО обойдётся?»

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

Шаблон репозитория, вероятно, является одним из самых простых вариантов в книге, если вы уже идёте по пути DDD и инверсии зависимостей. Что касается нашего кода, на самом деле мы просто меняем абстракцию SQLAlchemy (session.query (Batch)) на другую (batches_repo.get), которую мы разработали.

Нам придется добавлять несколько строк кода в нашем классе репозитория каждый раз, когда мы добавляем новый объект домена, который мы хотим получить, но взамен мы получаем простую абстракцию над нашим уровнем хранения, который мы контролируем. Шаблон репозитория позволит легко вносить фундаментальные изменения в то, как мы храним объекты (см. [appendix_csvs]), и, как мы увидим, его легко подменить для модульных тестов.

Кроме того, шаблон репозитория настолько распространен в мире DDD, что, если вы сотрудничаете с программистами, пришедшими в Python из мира Java и C#, они, скорее всего, узнают его. [repository_pattern_diagram] иллюстрирует этот паттерн.

images/apwp_0205.png
Figure 5. Repository pattern

Как всегда, мы начинаем с теста. Это, вероятно, было бы классифицировано как интеграционный тест, поскольку мы проверяем, что наш код (репозиторий) правильно интегрирован с базой данных; следовательно, тесты, как правило, смешивают необработанный SQL с вызовами и ассертами в нашем собственном коде.

TipВ отличие от предыдущих тестов ORM, эти тесты являются хорошими кандидатами на то, чтобы оставаться частью вашей кодовой базы в долгосрочной перспективе, особенно если какие-либо части вашей модели предметной области означают, что объектно-реляционная карта нетривиальна.
Example 9. Тест репозитория для сохранения объекта (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)  #1
    session.commit()  #2

    rows = list(session.execute(
        'SELECT reference, sku, _purchased_quantity, eta FROM "batches"'  #3
    ))
    assert rows == [("batch1", "RUSTY-SOAPDISH", 100, None)]
1repo.add() это тестируемый здесь метод.
2Мы храним .commit() вне репозитория и возлагаем ответственность на вызывающего. В этом есть свои плюсы и минусы; некоторые из причин станут яснее, когда мы доберемся до [chapter_06_uow].
3Используем необработанный SQL, чтобы убедиться, что были сохраненыправильные данные .

Следующий тест включает в себя извлечение пакетов и распределений, поэтому он более сложный:

Example 10. Тест репозитория для извлечения сложного объекта (test_repository.py)
def insert_order_line(session):
    session.execute(  #1
        '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):  #2
    ...

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)  #2

    repo = repository.SqlAlchemyRepository(session)
    retrieved = repo.get("batch1")

    expected = model.Batch("batch1", "GENERIC-SOFA", 100, eta=None)
    assert retrieved == expected  # Batch.__eq__ only compares reference  #3
    assert retrieved.sku == expected.sku  #4
    assert retrieved._purchased_quantity == expected._purchased_quantity
    assert retrieved._allocations == {  #4
        model.OrderLine("order1", "GENERIC-SOFA", 12),
    }
1Проверяет сторону чтения, поэтому необработанный SQL готовит данные для чтения repo.get().
2Избавляем вас от деталей insert_batch и insert_allocation; Зыдача в том, чтобы создать пару партий, а для интересующей нас партии выделить одну существующую строку заказа.
3Вот что мы здесь проверяем. Первый assert == проверяет соответствие типов и совпадение ссылок (потому что, как вы помните, Batch — это сущность, и для нее у нас есть собственный eq ).
4Поэтому мы также явно проверяем его основные атрибуты, в том числе ._allocations, который представляет собой набор Python-объектов значений OrderLine.

Независимо от того, насколько вы кропотливо написали тесты для каждой модели. После того, как у вас будет протестирован один класс на создание/изменение/сохранение, вы можете продолжить и протестировать другие с минимальным тестом на обратную связь или вообще ничего, если все они следуют схожему шаблону. В нашем случае конфигурация ORM, которая устанавливает набор ._allocations, немного сложна, поэтому заслуживает особого тестирования.

Вы получите что-то вроде этого:

Example 11. Типичный репозиторий (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 может выглядеть примерно так:

Example 12. Использование нашего репозитория непосредственно в нашей конечной точке API
Упражнение для читателя

На днях мы столкнулись с другом на конференции DDD, который сказал: "Я не использовал ORM в течение 10 лет." Шаблон репозитория и ORM действуют как абстракции перед необработанным SQL, поэтому использование одного за другим на самом деле не является необходимым. Почему бы не попробовать реализовать наш репозиторий без использования ORM? Вы найдете код на GitHub.

Мы оставили тесты репозитория, но решать, какой SQL писать, решать вам. Возможно, это будет труднее, чем вы думаете; возможно будет легче. Но хорошо то, что остальной части вашего приложения это до лампочки.

Создание поддельного репозитория для тестов теперь тривиально!

Вот одно из самых больших преимуществ шаблона репозиторий:

Example 13. Простой фейковый репозиторий с использованием набора (repository.py)

Поскольку это простая оболочка для set, все методы являются однострочными.

Использовать фальшивое репо в тестах действительно просто, и у нас есть простая абстракция, которую легко использовать и рассуждать:

Example 14. Пример использования поддельного репозитория (test_api.py)

Вы увидите эту подделку в действии в следующей главе.

TipСоздание подделок для ваших абстракций - отличный способ получить обратную связь от дизайна: если подделать сложно, значит, абстракция слишком сложна.

Что такое порт и что такое адаптер в Python?

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

Порты и адаптеры вышли из мира OO, и определение, которое мы придерживаемся, состоит в том, что port — это interface между нашим приложением и тем, что мы хотим абстрагировать, а adapter — это implementation (реализация) за этим интерфейсом или абстракцией.

Python не имеет интерфейсов как таковых, поэтому, хотя обычно легко идентифицировать адаптер, определение порта может быть сложнее. Если вы используете абстрактный базовый класс, это порт. Если нет, то порт—это просто duck type, которому соответствуют ваши адаптеры и который ожидает ваше основное приложение — имена используемых функций и методов, а также имена и типы их аргументов.

Конкретно, в этой главе, AbstractRepository это порт, a SqlAlchemyRepository и FakeRepository - это адаптеры.

Заключение

Помня цитату Рича Хики, в каждой главе мы суммируем затраты и преимущества каждого представленного архитектурного шаблона. Мы хотим, чтобы было ясно, что мы не говорим, что каждое отдельное приложение должно быть построено именно таким образом; только иногда сложность приложения и домена заставляет тратить время и усилия на добавление этих дополнительных слоев косвенности.

Имея это в виду, [chapter_02_repository_tradeoffs] показывает некоторые плюсы и минусы шаблона репозитория и нашей модели с игнорированием персистентности.

Table 1. Шаблон репозитория и persistence ignorance: компромиссы
ПлюсыМинусы
  • У нас есть простой интерфейс между persistent (постоянным) хранилищем и нашей доменной моделью.

  • Легко создать фейковую версию репозитория для модульного тестирования или заменить другие решения для хранения, потому что мы полностью отделили модель от проблем инфраструктуры.

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

  • Наша схема базы данных очень проста, потому что у нас есть полный контроль над тем, как мы сопоставляем наши объекты с таблицами.

  • ORM уже окупает вам затраты. Смена внешних ключей может создать сложности, но при необходимости будет довольно легко переключаться между MySQL и Postgres.

  • Ведение сопоставлений ORM вручную требует дополнительной работы и дополнительного кода.

  • Любой дополнительный уровень косвенности всегда увеличивает затраты на обслуживание и добавляет "фактор WTF" для программистов Python, которые никогда раньше не видели шаблон репозитория.

[domain_model_tradeoffs_diagram] демонстрирует основной тезис: да, для простых случаев развязанная модель предметной области является более сложной работой, чем простой шаблон ORM/ActiveRecord.[8]

TipЕсли ваше приложение представляет собой простую оболочку CRUD (создание-чтение-обновление-удаление) вокруг базы данных, вам не нужна модель предметной области или репозиторий.

Но чем сложнее домен, тем больше окупаются инвестиции в избавление от проблем с инфраструктурой с точки зрения простоты внесения изменений.

images/apwp_0206.png
Figure 6. Компромиссы модели предметной области в виде диаграммы

Наш пример кода не настолько сложен, чтобы дать больше, чем намек на то, как выглядит правая часть графика, но намеки есть. Представьте себе, например, что однажды мы решим, что хотим изменить распределение, чтобы жить на "OrderLine", а не на "Batch" объекте: если бы мы использовали, скажем, Django, нам пришлось бы определить и продумать миграцию базы данных, прежде чем мы могли бы запустить какие-либо тесты. Как бы то ни было, поскольку наша модель-это просто старые объекты Python, мы можем изменить set() на новый атрибут, не думая о базе данных до более подходящего момента.

Резюме шаблона репозитория
Применение инверсии зависимости в ORM

Наша модель предметной области должна быть свободна от проблем с инфраструктурой, поэтому ваш ORM должен импортировать вашу модель, а не наоборот.

Шаблон репозитория — это простая абстракция вокруг постоянного хранилища

Репозиторий дает вам иллюзию коллекции объектов в памяти. Это позволяет легко создать "FakeRepository" для тестирования и поменять местами основные детали вашей инфраструктуры, не нарушая работу вашего основного приложения. Смотрите [appendix_csvs] для примера.

Вам будет интересно, как мы создаем экземпляры этих хранилищ, поддельные или настоящие? Как на самом деле будет выглядеть наше приложение Flask? Вы узнаете об этом в следующей захватывающей части, the Service Layer pattern.

Но сначала небольшое отступление.


1. Полагаю, мы имеем в виду «отсутствие зависимостей с отслеживанием состояния». В зависимости от вспомогательной библиотеки это нормально; в зависимости от ORM или веб-фреймворка — нет.
2. Mark Seemann has an excellent blog post on the topic.
3. В этом смысле использование ORM уже является примером DIP. Вместо того чтобы полагаться на жестко запрограммированный SQL, мы зависим от абстракции, ORM. Но нам этого мало — не в этой книге!
4. Даже в проектах, где мы не используем ORM, мы часто используем SQLAlchemy вместе с Alembic для декларативного создания схем в Python и управления миграциями, соединениями и сеансами.
5. Привет чрезвычайно полезным специалистам по сопровождению SQLAlchemy и, в частности, Майку Байеру.
6. Вы можете подумать: «А как насчет listdelete или update?" Однако в идеальном мире мы модифицируем объекты нашей модели по одному, а удаление обычно обрабатывается как мягкое удаление, то есть batch.cancel (). Наконец, об обновлении позаботится шаблон Unit of Work, как вы увидите в [chapter_06_uow].
7. Чтобы действительно воспользоваться преимуществами ABC (какими бы они ни были), запустите помощники, такие как pylint и mypy.
8. Диаграмма вдохновлена ​​публикацией под названием «Глобальная сложность, локальная простота» Роба Венса.

Комментарии

Популярные сообщения из этого блога

4.Наш первый Use Case или пример использования: Flask API и Service Layer

Введение