Несмотря на довольно общее название данной статьи, перед нашей командой - Ducks Team встала конкретная задача…
Немного контекста для её понимания:
Мы разрабатываем платформу для Dungeons & Dragons, придерживаясь DDD (Domain-Driven Design) и TDD (Test-Driven Development) методологий. В качестве первого контекста мы выбрали - Базу знаний и практически закончили описывать её агрегаты. Перед тем как переходить к описанию наших use cases, нам нужно выделить логику работы с хранилищем данных. А для этого довольно хорошо подходит паттерн Репозиторий. Но что же выбрать для описания этого паттерна в коде - ABC или Protocol?…
Раскрытие этой темы не является чем-то обязательным в рамках данной статьи. Но я всё же решила немного осветить её здесь, преследуя две цели. Во-первых, я бы хотела, чтобы любой читатель, независимо от уровня его подготовки, мог поразмышлять над нашей задачей. Что было бы непросто, если не до конца понимать, в чём суть самого паттерна. Во-вторых, никогда не будет лишним прояснить, что именно понимается под тем или иным термином, чтобы избежать возможных недопониманий.
Если очень коротко, то паттерн Репозиторий обычно используют в DDD, чтобы отделить логику хранения данных от бизнес-логики самой предметной области. Также бизнес-логика не должна зависеть от того как и где мы храним данные. Используя паттерн Репозиторий, мы инвертируем зависимости таким образом, что логика хранения данных зависит от нашей бизнес-логики, а не наоборот. Например, в Django используется паттерн Active Record вместо паттерна Репозиторий. Это значит, что каждая модель имеет методы для CRUD операций и как следствие жёстко связана с базой данных, чего мы хотели бы избежать.

Чтобы использовать этот паттерн в своём проекте нам нужны два класса - Класс, объявляющий методы, необходимые нам для работы с хранилищем данных (обычно без реализации внутри). И второй класс с конкретной реализацией данных методов.
Небольшой пример того, как это может выглядеть (псевдокод, чтобы не фокусироваться на деталях):
# =====================================
# ПАТТЕРН РЕПОЗИТОРИЙ - ПСЕВДОКОД
# =====================================
# 1. СЛОЙ КОНТРОЛЛЕРА (API)
# Принимает HTTP-запросы, возвращает HTTP-ответы
def create_spell_endpoint(request: CreateSpellRequest):
dto = map_to_dto(request)
spell = use_case.execute(dto)
return map_to_response(spell)
# 2. СЛОЙ БИЗНЕС-ЛОГИКИ (USE CASE)
# Содержит логику приложения, не знает о деталях БД
class CreateSpellUseCase:
# Зависимость от абстракции, а не от реализации (DIP)
def __init__(self, repository: ISpellRepository):
self.repository = repository
def execute(dto: CreateSpellDTO):
spell = create_entity_from_dto(dto)
saved_spell = repository.save(spell)
return map_to_dto(saved_spell)
# 3. СЛОЙ ДОСТУПА К ДАННЫМ
# 3.1 Интерфейс (контракт) - ЧТО делать
class ISpellRepository:
def save(spell: Spell) -> Spell: pass
def get_by_id(id: int) -> Spell: pass
def delete(id: int) -> bool: pass
# 3.2 Реализация - КАК делать
class SpellRepository(ISpellRepository):
def save(spell: Spell):
# Здесь работа с БД: SQL, ORM и т.д.
return saved_spell
# =====================================
# ПОТОК ДАННЫХ:
# HTTP Request → API DTO → Use Case DTO → Entity → Repository → БД
# БД → Repository → Entity → Use Case DTO → API DTO → HTTP Response
# =====================================
Класс ISpellRepository, объявляющий методы, обычно называют - Интерфейсом, портом или абстракцией.
А класс SpellRepository, который их реализует, обычно называют - Имплементацией, адаптером, реализацией.
В нашем CreateSpellUseCase мы используем интерфейс, а не конкретную реализацию. Зависимости направлены к бизнес-логике, а не наоборот.
Подмена интерфейса на конкретную реализацию во время выполнения происходит автоматически, благодаря магии DI-контейнеров (но объяснение принципа их работы точно выходит за рамки данной статьи).
Использование паттерна Репозиторий даёт нам сразу несколько плюсов: