Repository commonly refers to a storage location, often for safety or preservation.
— Wikipedia
Вот как Википедия описывает репозиторий. Так уж случилось, что в отличие от некоторых других жаргонных словечек, с которыми мы имеем дело, этот термин прекрасно передает свою суть. Репозиторий представляет собой концепцию хранения коллекции для сущностей определенного типа.
Репозиторий как коллекция
Вероятно, наиболее важным отличием репозиториев является то, что они представляют собой коллекции объектов. Они не описывают хранение в базах данных или кэширование или решение любой другой технической проблемы. Репозитории представляют коллекции. Как вы храните эти коллекции — это просто деталь реализации.
Я хочу внести ясность в этот вопрос. Репозиторий — это коллекция. Коллекция, которая содержит сущности и может фильтровать и возвращать результат обратно в зависимости от требований вашего приложения. Где и как он хранит эти объекты является ДЕТАЛЬЮ РЕАЛИЗАЦИИ.
В мире PHP мы привыкли к циклу запрос/ответ, который оканчивается смертью процесса. Все, что пришло извне и не сохранилось — ушло навсегда, в этой точке. Так вот, не все платформы работают именно так.
Хорошим способом понять как работают репозитории является представление вашего приложения постоянно работающим, в этом случае все объекты остаются в памяти. Вероятность критических сбоев и реакцию на них в этом эксперименте можно пренебречь. Представьте, что у вас есть Singleton-экзепляр репозитория для сущностей Member
, MemberRepository
.
Затем создайте новый объект Member
и добавьте его в репозиторий. Позже, вы запросите у репозитория все элементы, хранящиеся в нем, таким образом вы получите коллекцию, которая содержит этот объект внутри. Возможно вы захотите получить какой-то конкретный объект по его ID, это также возможно. Очень легко представить себе, что внутри репозитория эти объекты хранятся в массиве или, что еще лучше, в объекте-коллекции.
Проще говоря, репозиторий — это особый вид надежных коллекций, которые вы будете использовать снова и снова, чтобы хранить и фильтровать сущности.
Взаимодействие с Репозиторием
Представьте, что мы создаем сущность Member
. Мы приводим объект к необходимому состоянию, затем запрос заканчивается и объект исчезает. Пользователь пытается авторизоваться в нашем приложении и не может. Очевидно, что нам необходимо сделать этот объект доступным и для других частей приложения.
$member = Member::register($email, $password);
$memberRepository->save($member);
Теперь мы можем получить доступ к объекту позже. Примерно так:
$member = $memberRepository->findByEmail($email);
// or
$members = $memberRepository->getAll();
Мы можем хранить объекты в одной части нашего приложения, а затем извлекать их из другой.
Должны ли репозитории создавать сущности?
Вы можете встретить такие примеры:
$member = $memberRepository->create($email, $password);
Я видел множество аргументов приводящихся в пользу этого, но совершенно не заинтересован в подобном подходе.
Прежде всего, репозитории — это коллекции. Я не уверен в том, зачем коллекция должна быть коллекцией и фабрикой. Я слышал аргументы вроде «если обращаться удобнее так, то почему бы не повесить обработчик на подобные действия»?
На мой взгляд, это анти-паттерн. Почему бы не позволить классу Member
, иметь свое собственное понимание как и зачем создается объект или почему бы не сделать отдельную фабрику для создания более сложных объектов?
Если мы относимся к нашим репозиториям как к простым коллекциям, так значит и не нужно нагружать их лишним функционалом. Я не хочу классов коллекций, которые ведут себя как фабрики.
В чем выгода использования репозиториев?
Основное преимущество репозиториев — это абстрактный механизм хранения для коллекций сущностей.
Предоставляя интерфейс MemberRepository
мы развязываем руки разработчику, который уже сам решит как и где хранить данные.
interface MemberRepository {
public function save(Member $member);
public function getAll();
public function findById(MemberId $memberId);
}
class ArrayMemberRepository implements MemberRepository {
private $members = [];
public function save(Member $member) {
$this->members[(string)$member->getId()] = $member;
}
public function getAll() {
return $this->members;
}
public function findById(MemberId $memberId) {
if (isset($this->members[(string)$memberId])) {
return $this->members[(string)$memberId];
}
}
}
class RedisMemberRepository implements MemberRepository {
public function save(Member $member) {
// ...
}
// you get the point
}
Таким образом, большинство наших приложений знает только абстрактное понятие MemberRepository и его использование может быть отделено от фактической реализации. Это очень раскрепощает.
К чему относятся репозитории: Domain или Application Service Layer?
Итак, вот интересный вопрос. Во-первых, давайте определим, что Application Service Layer
— это многоуровневая архитектура, которая отвечает за специфические детали реализации приложения, такие как целостность базы данных, и различные релазиации работы с интернет-протоколами (отправка электронной почты, API) и др.
Определим термин Domain Layer
как слой многоуровневой архитектуры, которая отвечает за бизнес-правила и бизнес-логику.
Куда же попадет репозиторий при таком подходе?
Давайте посмотрим на нашем примере. Вот код, написанный ранее.
class ArrayMemberRepository implements MemberRepository {
private $members = [];
public function save(Member $member) {
$this->members[(string) $member->getId()] = $member;
}
public function getAll() {
return $this->members;
}
public function findById(MemberId $memberId) {
if (isset($this->members[(string)$memberId])) {
return $this->members[(string)$memberId];
}
}
}
В этом примере я вижу много деталей реализации. Они, несомненно, должны входить в слой приложения
А теперь давайте удалим все детали реализации из этого класса…
class ArrayMemberRepository implements MemberRepository {
public function save(Member $member) {
}
public function getAll() {
}
public function findById(MemberId $memberId) {
}
}
Хм… это начинает выглядеть знакомо… Что же мы забыли?
Возможно, получившийся код напоминает вам это?
interface MemberRepository {
public function save(Member $member);
public function getAll();
public function findById(MemberId $memberId);
}
Это означает, что интерфейс находится на границе слоев. и на самом деле может содержать доменно-специфические концепты, но сама реализация не должна этого делать.
Интерфейсы репозиториев принадлежат к слою домена. Реализация же относятся к слою приложения. Это означает, что мы свободны при построении архитектуры на уровне доменного слоя без необходимости зависеть от слоя сервиса.
Свобода смены хранилищ данных
Всякий раз, когда вы слышите чей-то разговор о концепции объектно-ориентированного дизайна, вы, наверное, могли слышать что-то вроде "… и у вас есть возможность поменять одну реализацию хранения данных на другую в будущем..."
По-моему, это не совсем правда… я бы даже сказал, что это очень плохой аргумент. Самой большой проблемой объяснения концепции репозиториев является то, что сразу напрашивается вопрос «вы действительно хотите это делать?». Я НЕ хочу чтобы подобные вопросы влияли на использование паттерна репозитория.
Любое достаточно хорошо спроектированное объектно-ориентированное приложение автоматически подходит под приведенное преемущество. Центральной концепцией ООП является инкапсуляция. Вы можете предоставить доступ к API и скрыть реализацию.
Ведь вы же на самом деле не будете переключаться с одного ORM на другой и обратно. Но даже если вы захотите так делать, то, по крайней мере, у вас будет возможность сделать это. Однако, замена реализации репозитория будет огромным плюсом при тестировании.
Тестирование при использовании паттерна «Репозиторий»
Ну, тут все просто. Давайте предположим, что у вас есть объект, который обрабатывает что-то вроде регистрации участников…
class RegisterMemberHandler {
private $members;
public function __construct(MemberRepository $members) {
$this->members = $members;
}
public function handle(RegisterMember $command) {
$member = Member::register($command->email, $command->password);
$this->members->save($member);
}
}
Во время очередной операции, я могу взять экземпляр DoctrineMemberRepository
. Однако, во время тестирования легко можно заменить его на экземпляр ArrayMemberRepository. Они оба реализуют один и тот же интерфейс.
Упрощенный пример теста может выглядеть примерно так…
$repo = new ArrayMemberRepository;
$handler = new RegisterMemberHandler($repo);
$request = $this->createRequest(['email' => 'bob@bob.com', 'password' => 'angelofdestruction']);
$handler->handle(RegisterMember::usingForm($request));
AssertCount(1, $repo->findByEmail('bob@bob.com'));
В этом примере мы тестируем обработчик. Нам не нужно проверять корректность хранения данных репозитория в БД (или еще где). Мы тестируем конкретное поведение этого объекта: регистрируем пользователя на основе данных формы, а затем передаем их в репозиторий.
Коллекция или Состояние
В книге Implementing Domain-Driven Design Vaughn Vernon делает различие между типами репозиториев. Идея коллекцио-ориентированного репозитория (ориг. — collection-oriented repository) в том, что работа с репозиторием идет в памяти, как с массивом. Репозиторий, ориентированный на хранение состояний (ориг. — persistence-oriented repository) содержит в себе идею, что в нем будет какая-то более глубокая и продуманная система хранения. По сути различия лишь в названиях.
// collection-oriented
$memberRepository->add($member);
// vs persistence-oriented
$memberRepository->save($member);
Замечу, что это лишь мое мнение и пока что я придерживаюсь именно его в вопросах использования репозиториев. Однако, хотел бы предупредить, что возможно могу передумать. В конце-концов, я сосредотачиваюсь на них как на коллекциях объектов с теми же обязанностями, что и у любого другого объекта-коллекции.
Дополнительная информация
everzet создал проект на Github о репозиториях на который, безусловно, стоит посмотреть. Внутри вы найдете примеры работы с хранением в памяти и файлах.
Итоги
Я считаю, что…
- … важно дать репозиториям сингулярную задачу функционировать как коллекция объектов.
- … мы не должны использовать репозитории для создания новых экземпляров объектов.
- … мы должны избегать использования репозиториев как способа перехода от одной технологии к другой, так как они имеют очень много преимуществ, от которых трудно отказаться.
В будущем я планирую написать еще несколько статей о репозиториях, таких как кэширование результатов с помощью декоратора, запросов с помощью паттерна критерия, роли репозитория в обработке пакетных операций на большом количестве объектов.
Если у вас есть вопросы или если ваше мнение отличается от моего, пожалуйста, пишите комментарии ниже.
Как всегда, я намерен обновлять статью, чтобы синхронизировать ее с моим текущим мнением.
Автор: iGusev