← Заметки и статьи о Laravel PHP

4 марта 2015 перевод

Паттерн Репозиторий

Репозиторий обычно используется как хранилище данных, часто для обеспечения безопасности или сохранности — Википедия.

Вот как Википедия описывает репозитории. И так сложилось, что в отличии от других различных определений с которыми мы сталкиваемся — это подходит идеально. Репозиторий олицетворяет концепцию хранилища коллекции конкретного типа сущности.

Репозиторий как коллекция

Вероятно самое важное свойство репозиториев это то, что они олицетворяют коллекцию сущностей. Они не являются хранилищем в базе данных или кэше, или тому подобному. Репозитории являются коллекциями. Как вы используете эти коллекции — это просто детали реализации.

Я хочу немного прояснить на этой стадии. Репозиторий это коллекция, коллекция которая содержит сущности, которые могут быть как либо отфильтрованы и возвращены назад в зависимости от требований вашего приложения. Как именно они содержат эти сущности — это ДЕТАЛЬ РЕАЛИЗАЦИИ.

В мире PHP мы используем цикл Запрос/Ответ, сопровождающийся смертью PHP процесса. Всё, что не хранится внешне уничтожается навсегда в этом случае. Сейчас не все платформы работают по этому принципу.

Я нахожу хорошим мысленным экспериментом для того чтобы понять репозитории, это представить что ваше приложение всегда запущено и что объекты всегда остаются в памяти. Мы не беспокоимся о критических проблемах в этом эксперименте. Представьте что у вас есть одиночный репозиторий для сущности MemberMemberRepository.

Затем вы создаёте нового 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);
    }
}

В ходе обычных операций вы можете сделать инъекцию реализации MemberRepositoryDoctrineMemberRepository. Тем не менее в ходе тестирования вы можете заменить её на ArrayMemberRepository. Обе они реализуют интерфейс

Упрощённая версия теста может быть такой..

 <?php
 
 $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'));

В этом примере мы тестируем обработчик. Нам не нужно тестировать то что репозиторий хранит данные в базе или где-то ещё. Нам нужно протестировать поведение этого объекта, который должен запросить у класса Member нового участника на основе аргументов команды, затем положить его в репозиторий.

Коллекцие-Ориентированный или Состояние-Ориентированный

В книге Реализация DDD, Vaughn Vernon делает различие между основанными на состоянии и на коллекциях репозиториями. Вкратце, идея репозиториев основанных на коллекциях это то, что с данными обращаются как с хранилищем массива в памяти. Но в ориентированном на состоянии репозитории всё сводится к тому что данных хранятся глубже. Это исходит из их названий.

У меня сейчас нет мнения по поводу того, который использовать. Тем не менее, я осторожен с этим. Сейчас я фокусируюсь на репозиториях как на коллекциях объектов с такой же ответственностью какую и любая другая коллекция объектов может иметь.

Итог

Я верю в то что...

.. важно дать репозиториям единственное задание — функционировать как коллекция объектов

.. мы не должны использовать репозитории для создания сущностей

.. мы должны легко уметь менять технологию с помощью которой мы получаем данные, так как это приносить массу преимуществ, которые сложно недооценить

В будущем я хочу написать ещё несколько статей о репозиториях, например о том как кешировать результаты репозитория используя паттерн декоратор, проведение запросов с помощью паттерна критерия, роль репозитория в сборке в одно целое вариантов реализации, и реализация группы операций с большим числом объектов.

Источник: http://shawnmc.cool/the-repository-pattern

Плюсануть
Поделиться
Отправить