Практика 13. Практическая работа 13. Репозиторий и Unit of Work
Цель
- Закрепить паттерн Repository как абстракцию над источником данных.
- Научиться писать несколько взаимозаменяемых реализаций одного интерфейса репозитория (в памяти, имитация БД, мок для тестов).
- Освоить паттерн Unit of Work с операциями
commitиrollback. - Отработать подмену реализации хранилища и тестирование бизнес-логики с использованием мока, без поднятия настоящей базы данных.
Краткая теория
Repository скрывает работу с хранилищем за интерфейсом, который выглядит
как коллекция объектов в памяти. Бизнес-логика зависит от абстракции
(ABC/Protocol), а не от конкретного класса, поэтому источник данных
(память, БД, файл, API) можно подменять без правки логики.
Поскольку все реализации подчиняются одному контракту, для тестов удобно
сделать мок — заглушку с контролируемым поведением, которая возвращает
заранее заданные данные и фиксирует факт вызова (save, delete).
Unit of Work («единица работы») накапливает изменения в рамках одной
бизнес-операции (новые, изменённые, удалённые объекты) и применяет их единым
блоком: либо всё (commit), либо ничего (rollback). По сути это объект,
управляющий транзакцией поверх одного или нескольких репозиториев.
from abc import ABC, abstractmethodfrom typing import Optional, List
class Repository(ABC): @abstractmethod def find_by_id(self, item_id: int) -> Optional[object]: ... @abstractmethod def save(self, item) -> object: ... @abstractmethod def delete(self, item_id: int) -> None: ...Подробности — в лекции 14 («Репозиторий, Unit of Work и Dependency Injection»).
Задания
Задание 1. Интерфейс репозитория и реализация в памяти
Опишите модель и абстрактный репозиторий, затем реализацию в памяти.
Требования:
- Модель
Product(dataclass):id: Optional[int],name: str,price: float,category: str,stock: int. - Абстрактный класс
ProductRepository(ABC)с методами:find_by_id(product_id: int) -> Optional[Product]find_all() -> List[Product]save(product: Product) -> Productdelete(product_id: int) -> Noneexists(product_id: int) -> bool
InMemoryProductRepository(ProductRepository)— хранение вdict, автогенерацияidприsave, еслиproduct.id is None.
Скелет:
class ProductRepository(ABC): @abstractmethod def find_by_id(self, product_id: int) -> Optional[Product]: ... # ... остальные методы
class InMemoryProductRepository(ProductRepository): def __init__(self): self._storage: dict[int, Product] = {} self._next_id = 1 # реализуйте все методы интерфейсаПокажите в if __name__ == "__main__" сохранение трёх продуктов, поиск по
id и вывод find_all().
Задание 2. Имитация БД и подмена реализации
Реализуйте тот же интерфейс поверх «базы данных» (имитация) и докажите взаимозаменяемость.
Требования:
DatabaseProductRepository(ProductRepository):- конструктор принимает
connection_string: str; - каждый метод печатает соответствующий «SQL» (
SELECT,INSERT,UPDATE,DELETE) и работает с внутреннимdict, имитируя таблицу; saveразличаетINSERT(нет id) иUPDATE(id задан).
- конструктор принимает
- Сервис
ProductService, который зависит только отProductRepository(внедрение через конструктор):add_product(name, price, category, stock) -> Product;get_catalog() -> List[Product].
- Функция
run_service(repo: ProductRepository), которая принимает любую реализацию и выполняет один и тот же сценарий.
Требование к проверке: вызовите run_service сначала с
InMemoryProductRepository, затем с DatabaseProductRepository —
бизнес-код не должен меняться. Подмена реализации происходит в одной точке.
Задание 3. Мок репозитория и тесты бизнес-логики
Напишите мок и протестируйте сервис без настоящего хранилища.
Требования:
FakeProductRepository(ProductRepository):- конструктор принимает
preset: List[Product] = None; - хранит элементы в
dict, заполняется изpreset; - атрибут
self.saved: List[Product]— список всех сохранённых объектов; - атрибут
self.deleted: List[int]— список удалённых id.
- конструктор принимает
- Тесты на
pytest(или функцииassert), которые проверяют:- после
service.add_product(...)объект попал вrepo.saved; get_catalog()возвращает элементы изpreset;- сервис ни разу не обращается к настоящей БД (используется только мок).
- после
Скелет теста:
def test_add_product_saves_to_repository(): repo = FakeProductRepository() service = ProductService(repo) service.add_product("Мышь", 1000, "Электроника", 5) assert len(repo.saved) == 1 assert repo.saved[0].name == "Мышь"Мок подменяет хранилище, поэтому тест не зависит от внешних ресурсов и выполняется мгновенно.
Задание 4. Unit of Work с commit/rollback
Реализуйте единицу работы поверх репозитория продуктов.
Требования:
- Абстрактный
UnitOfWork(ABC)с методамиcommit()иrollback(). ProductUnitOfWork(UnitOfWork):- конструктор принимает
repository: ProductRepository; register_new(product),register_modified(product),register_deleted(product_id)— накапливают изменения в трёх списках;commit()применяет изменения к репозиторию (save/delete); при любом исключении вызываетrollback()и пробрасывает ошибку;rollback()очищает накопленные изменения (ничего не применяется).
- конструктор принимает
- Сценарий-демонстрация: зарегистрируйте новый, изменённый и удаляемый
объекты, затем
commit(); проверьте, что изменения попали в репозиторий.
Требование к проверке отката: смоделируйте сбой (например, репозиторий с
save, бросающим исключение на определённом объекте) и убедитесь, что после
неудачного commit накопленные списки очищены, а исключение проброшено.
Задание 5*. Unit of Work как контекстный менеджер
Расширьте ProductUnitOfWork поддержкой with.
Требования:
- Реализуйте
__enter__(возвращаетself) и__exit__:- при отсутствии исключения — автоматический
commit(); - при исключении — автоматический
rollback()и проброс ошибки.
- при отсутствии исключения — автоматический
- Покажите два сценария:
- успешный блок
with uow:— изменения сохранены; - блок с
raiseвнутри — изменения откачены, хранилище не тронуто.
- успешный блок
- Напишите тест с
FakeProductRepository, проверяющий, что при ошибке внутриwithсписокrepo.savedостался пустым.
Пример использования:
with ProductUnitOfWork(repo) as uow: uow.register_new(Product(None, "Ноутбук", 50000, "Электроника", 3)) uow.register_deleted(7)# при выходе из блока без ошибок — commit() вызван автоматическиКритерии оценки
- Задание 1 (интерфейс + InMemory) — 20%: корректный
ABC, рабочая реализация в памяти, автогенерация id. - Задание 2 (имитация БД + подмена) — 20%: общий интерфейс, сервис зависит от абстракции, доказана взаимозаменяемость реализаций.
- Задание 3 (мок + тесты) — 20%: мок фиксирует вызовы, тесты проходят без обращения к БД.
- Задание 4 (Unit of Work) — 25%: накопление изменений, корректные
commit/rollback, обработка сбоя с откатом. - Задание 5* (контекстный менеджер) — 15%: автоматические
commit/rollback через
with, тест отката. - Чистота кода, docstrings и осмысленные имена учитываются в каждом пункте.
Максимум без задания 5* — 85%, со звёздочкой — 100%.
Вопросы для самопроверки
- Какую задачу решает паттерн Repository и почему бизнес-логике лучше зависеть от его интерфейса, а не от конкретной реализации?
- Чем реализация репозитория в памяти удобна для прототипов и тестов?
- Почему
InMemoryProductRepositoryиDatabaseProductRepositoryвзаимозаменяемы в сервисе? В какой точке выбирается реализация? - Что отличает мок от обычной реализации репозитория и какие данные он обычно фиксирует?
- Зачем мок хранит списки
savedиdeleted? Что они позволяют проверить в тесте? - Какие три категории изменений накапливает Unit of Work и в каком порядке
их разумно применять при
commit? - Что произойдёт в
commit, если при сохранении одного из объектов возникнет исключение? - Чем
rollbackотличается от простого «ничего не делать» и как он связан с понятием транзакции? - Какие преимущества даёт реализация Unit of Work через контекстный
менеджер (
with) по сравнению с ручными вызовами commit/rollback?