Репозиторий обычно используется как хранилище данных, часто для обеспечения безопасности или сохранности — Википедия.
Вот как Википедия описывает репозитории. И так сложилось, что в отличии от других различных определений с которыми мы сталкиваемся — это подходит идеально. Репозиторий олицетворяет концепцию хранилища коллекции конкретного типа сущности.
Репозиторий как коллекция
Вероятно самое важное свойство репозиториев это то, что они олицетворяют коллекцию сущностей. Они не являются хранилищем в базе данных или кэше, или тому подобному. Репозитории являются коллекциями. Как вы используете эти коллекции — это просто детали реализации.
Я хочу немного прояснить на этой стадии. Репозиторий это коллекция, коллекция которая содержит сущности, которые могут быть как либо отфильтрованы и возвращены назад в зависимости от требований вашего приложения. Как именно они содержат эти сущности — это ДЕТАЛЬ РЕАЛИЗАЦИИ.
В мире PHP мы используем цикл Запрос/Ответ, сопровождающийся смертью PHP процесса. Всё, что не хранится внешне уничтожается навсегда в этом случае. Сейчас не все платформы работают по этому принципу.
Я нахожу хорошим мысленным экспериментом для того чтобы понять репозитории, это представить что ваше приложение всегда запущено и что объекты всегда остаются в памяти. Мы не беспокоимся о критических проблемах в этом эксперименте. Представьте что у вас есть одиночный репозиторий для сущности Member
— MemberRepository
.
Затем вы создаёте нового Member
и добавляете его в репозиторий. Позже вы запрашиваете у репозитория всех members
и получаете назад коллекцию, которая содержит Member
которого вы добавили. Возможно вы захотите получить отдельного Member
по ID
, вы можете сделать это тоже. Легко представить что внутри репозитория эти объекты Member
хранятся как массив или лучше как коллекция объектов.
Проще говоря, репозиторий это специальный тип управляющей коллекции, которую вы используете снова и снова для хранения и получения обратно сущностей.
Взаимодействие с репозиторием
Представьте что вы создали сущность Member
. Вы удовлетворены объектом Member
, затем когда запрос заканчивается, объект Member
исчезает. Затем Member
пытается авторизироваться в вашем приложении и не может. Очевидно что мы должны сделатьMember
доступным в других частях нашего приложения.
<?php
$member = Member::register($email, $password);
$memberRepository->save($member);
Затем мы захотели получить Member
позже, например так:
<?php
$member = $memberRepository->findByEmail($email);
// или
$members = $memberRepository->getAll();
Теперь мы можем хранить объекты Member
в одной части нашего приложения, и затем получать их в другой части.
Должен ли репозиторий создавать сущности?
Вы могли делать что-то вроде этого:
<?php
$member = $memberRepository->create($email, $password);
Я видел людей которые приводили аргументы для этого подхода. Но я крайне не рекомендую его.
Ещё раз, репозитории это коллекции. Я не уверен что коллекция должна быть ещё и фабрикой. Я слышал аргументы типа.. они хранят состояние, почему же не хранить ещё и создание сущностей?
В моём разуме это анти-паттерн. Почему не разрешить Member
иметь свои собственные представления о своём создании, или почему не иметь фабрику которая специально разработана для обеспечения создания более комплексных объектов.
Если мы используем наши репозитории как простые коллекции, тогда мы даём им одну ответственность. Я не хочу классы коллекции которые так же являются фабриками.
В чём преимущество репозиториев?
Главное преимущество репозиториев — это абстрактный механизм хранилища для управляющей коллекции сущностей.
Когда мы создаём интерфейс MemberRepository
, мы разрешаем существование любого числа его конкретных реализаций:
<?php
interface MemberRepository {
public function save(Member $member);
public function getAll();
public function findById(MemberId $memberId);
}
Первая реализация:
<?php
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];
}
}
}
И ещё одна:
<?php
class RedisMemberRepository implements MemberRepository {
public function save(Member $member) {
// ...
}
// you get the point
}
Если идти по этому пути, то наше приложения знает только абстрактную концепциюMemberRepository
и наше использование этого репозитория может быть разделено на несколько реализаций. Это вполне гибко.
Репозитории принадлежат к слою домена или слою приложения?
Это достаточно интересный вопрос. Во-первых, давайте определим слой приложения как многослойную архитектуру, которая ответственна за реализацию конкретных деталей работы приложения, таких как работа с БД, знания о протоколе передачи данных(отправка email, взаимодейcтвие с API) и др.
Давайте определим слой домена как многослойную архитектуру, которая ответственна за хранение бизнес-правил и бизнес-логики.
Работая с этими определениями, в которое из них подходит наш репозиторий?
Давайте посмотрим на наш пример из кода выше:
<?php
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];
}
}
}
В этом примере я вижу множество деталей реализации. Эти детали реализации безусловно относятся к слою приложения.
Давайте уберём все детали реализации из этого класса…
<?php
class ArrayMemberRepository implements MemberRepository {
public function save(Member $member) {
}
public function getAll() {
}
public function findById(MemberId $memberId) {
}
}
Хм, это кажется достаточно знакомым. Где же это было?
Может быть это напоминает вам это?
<?php
interface MemberRepository {
public function save(Member $member);
public function getAll();
public function findById(MemberId $memberId);
}
Это значит что интерфейс находится на границе слоёв. Сам интерфейс может содержать специфичные для домена концепции, но реализация интерфейса не должна.
Интерфейс репозитория принадлежит к слою домена. Реализация интерфейса принадлежит к слою приложения. Это значит что мы можем писать подсказки к нашим репозиториям в слое домена даже не дотрагиваясь к слою приложения.
Свобода выбора хранилища данных
Вы наверное слышали в разговорах о концепциях объектно-ориентированного программирования что-то типа такого — «и вы полностью неограничены в смене одного хранилища данных на другое позже».
Я пришёл к выводу что это не совсем правда.. это очень слабый аргумент. Самая большая проблема с этим объяснением, это то, что оно приводит к вопросу — «А вы действительно захотите менять хранилище данных?». Я не хочу чтобы ответ на этот вопрос определял использовать или нет паттерн репозиторий.
Любое хорошо спроектированное объектно-ориентированное приложения автоматически идёт с этим типом преимущества. Центральная концепция объектной-ориентированности это инкапсуляция. Вы можете показать API и скрыть реализацию.
Правда в том, что вы наверное не захотите менять одну ORM на другую. Но у вас хотя бы будет хороший способ реализовать это. Тем не менее, смена реализаций репозиториев отлично подходит для тестирования.
Тестируемость с паттерном репозиторий
Угадайте что? Это очень вкусно. Допустим у вас есть объект, который содержит что-либо похожее на регистрацию участников:
<?php
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);
}
}
В ходе обычных операций вы можете сделать инъекцию реализации MemberRepository
—DoctrineMemberRepository
. Тем не менее в ходе тестирования вы можете заменить её наArrayMemberRepository
. Обе они реализуют интерфейс
Упрощённая версия теста может быть такой..
<?php
$repo = new ArrayMemberRepository;
$handler = new RegisterMemberHandler($repo);
$request = $this->createRequest(['email' => '[email protected]', 'password' => 'angelofdestruction']);
$handler->handle(RegisterMember::usingForm($request));
AssertCount(1, $repo->findByEmail('[email protected]'));
В этом примере мы тестируем обработчик. Нам не нужно тестировать то что репозиторий хранит данные в базе или где-то ещё. Нам нужно протестировать поведение этого объекта, который должен запросить у класса Member
нового участника на основе аргументов команды, затем положить его в репозиторий.
Коллекцие-Ориентированный или Состояние-Ориентированный
В книге Реализация DDD, Vaughn Vernon делает различие между основанными на состоянии и на коллекциях репозиториями. Вкратце, идея репозиториев основанных на коллекциях это то, что с данными обращаются как с хранилищем массива в памяти. Но в ориентированном на состоянии репозитории всё сводится к тому что данных хранятся глубже. Это исходит из их названий.
У меня сейчас нет мнения по поводу того, который использовать. Тем не менее, я осторожен с этим. Сейчас я фокусируюсь на репозиториях как на коллекциях объектов с такой же ответственностью какую и любая другая коллекция объектов может иметь.
Итог
Я верю в то что…
.. важно дать репозиториям единственное задание — функционировать как коллекция объектов
.. мы не должны использовать репозитории для создания сущностей
.. мы должны легко уметь менять технологию с помощью которой мы получаем данные, так как это приносить массу преимуществ, которые сложно недооценить
В будущем я хочу написать ещё несколько статей о репозиториях, например о том как кешировать результаты репозитория используя паттерн декоратор, проведение запросов с помощью паттерна критерия, роль репозитория в сборке в одно целое вариантов реализации, и реализация группы операций с большим числом объектов.