«Репозиторий обычно используется как хранилище данных, часто для обеспечения безопасности или сохранности» — Википедия.
Вот как Википедия описывает репозитории. И так сложилось, что в отличии от других различных определений с которыми мы сталкиваемся — это подходит идеально. Репозиторий олицетворяет концепцию хранилища коллекции конкретного типа сущности.
Вероятно самое важное свойство репозиториев это то, что они олицетворяют коллекцию сущностей. Они не являются хранилищем в базе данных или кэше, или тому подобному. Репозитории являются коллекциями. Как вы используете эти коллекции — это просто детали реализации.
Я хочу немного прояснить на этой стадии. Репозиторий это коллекция, коллекция которая содержит сущности, которые могут быть как либо отфильтрованы и возвращены назад в зависимости от требований вашего приложения. Как именно они содержат эти сущности — это ДЕТАЛЬ РЕАЛИЗАЦИИ.
В мире PHP мы используем цикл Запрос/Ответ, сопровождающийся смертью PHP процесса. Всё, что не хранится внешне уничтожается навсегда в этом случае. Сейчас не все платформы работают по этому принципу.
Я нахожу хорошим мысленным экспериментом для того чтобы понять репозитории, это представить что ваше приложение всегда запущено и что объекты всегда остаются в памяти. Мы не беспокоимся о критических проблемах в этом эксперименте. Представьте что у вас есть одиночный репозиторий для сущности Member — MemberRepository.
Затем вы создаёте нового Member и добавляете его в репозиторий. Позже вы запрашиваете у репозитория всех members и получаете назад коллекцию, которая содержит Member которого вы добавили. Возможно вы захотите получить отдельного Member по ID, вы можете сделать это тоже. Легко представить что внутри репозитория эти объекты 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, взаимодейтвие с 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 делает различие между основанными на состоянии и на коллекциях репозиториями. Вкратце, идея репозиториев основанных на коллекциях это то, что с данными обращаются как с хранилищем массива в памяти. Но в ориентированном на состоянии репозитории всё сводится к тому что данных хранятся глубже. Это исходит из их названий.
У меня сейчас нет мнения по поводу того, который использовать. Тем не менее, я осторожен с этим. Сейчас я фокусируюсь на репозиториях как на коллекциях объектов с такой же ответственностью какую и любая другая коллекция объектов может иметь.
Я верю в то что...
.. важно дать репозиториям единственное задание — функционировать как коллекция объектов
.. мы не должны использовать репозитории для создания сущностей
.. мы должны легко уметь менять технологию с помощью которой мы получаем данные, так как это приносить массу преимуществ, которые сложно недооценить
В будущем я хочу написать ещё несколько статей о репозиториях, например о том как кешировать результаты репозитория используя паттерн декоратор, проведение запросов с помощью паттерна критерия, роль репозитория в сборке в одно целое вариантов реализации, и реализация группы операций с большим числом объектов.
Идеально