Oc-windows.ru

IT Новости из мира ПК
0 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

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

Паттерн «Репозиторий» в Laravel

В этой статье я расскажу, как настроить с нуля паттерн Репозиторий (Repository, Хранилище) в Laravel. Использую версию Laravel 5.8, но по идее версия не имеет большого значения. Прежде, чем приступим к коду, вам нужно кое-что знать об этом шаблоне.

Репозиторий позволяет использовать объекты, не зная, как эти объекты сохраняются. По сути, это абстракция слоя данных.

Это означает, что вашей бизнес-логике не нужно знать, как извлекаются данные или каков их источник. Бизнес-логика полагается на репозиторий в получении правильных данных.

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

Хватит Теории, да будет Код

Поскольку мы делаем все с нуля, то начнем с создания нового проекта Laravel:

Для этого урока я сделаю небольшой блог. Создаем Контроллер и Модель для нашего блога.

Это создаст BlogController в папке app/Http/Controllers.

Примечание: Опция -m создает миграцию для базы данных. Этот файл можно найти в папке database/migrations.

Команда выше создаст модель Blog и сохранит ее в папке app/Models. Это лишь один из способов хранения моделей, но я предпочитаю именно его.

Теперь, когда у нас есть наш Контроллер и Модель, пришло время взглянуть на созданный нами файл миграции. Нашему блогу пока нужны только title (Заголовок), content (содержимое) и поле user_id, не считая дефолтных полей меток времени Laravel.

Примечание: Если вы используете Laravel старее чем 5.8, то вам следует заменить строку

Настройка базы данных

Для этого примера я буду использовать базу данных MySQL. Создаем новую базу.

Это создаст базу данных с именем laravel_repository. Теперь нужно добавить её учетные данные в файл .env.

После изменения файла .env надо очистить кеш конфигурации:

Запуск миграции

У нас есть настроенная база данных, можно запускать миграцию:

Это создаст таблицу blogs с полями title , content и user_id, которые мы объявили в миграции.

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

Теперь, когда все готово, можно приступать к реализации шаблона. Начнем с создания папки Repositories в папке app. Вторая папка, которую мы создадим, это папка Interfaces. Она будет находиться прямо в свеже созданной папке Repositories.

В папке Interfaces мы создаем класс BlogRepositoryInterface, который, на данный момент, содержит два метода:

  1. Метод all, который возвращает все блоги.
  2. Метод getByUser, который возвращает все блоги, конкретного пользователя.

Последний класс, который мы создадим, это BlogRepository, который реализует BlogRepositoryInterface. Будем придерживаться очень простой имплементации.

Папка Repositories должна выглядеть следующим образом:

Вы успешно создали свой Репозиторий! Но мы еще не закончили. Настало время использовать его.

Репозиторий в действии

Чтобы начать использовать BlogRepository, мы должны внедрить его в BlogController. Поскольку Репозиторий будет внедрен, то его легко можно заменить другой реализацией. Вот как будет выглядеть контроллер:

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

Шаблон проектирования Репозиторий позволяет легко переключаться между источниками данных. В этом примере мы используем базу данных для получения наших блогов. Мы полагаемся на Eloquent, который делает это за нас. Но допустим, где-то в Интернете мы видели отличный API для блогов, и мы хотим им воспользоваться. Все, что нам нужно, это переписать BlogRepository, чтобы он использовал этот API вместо Eloquent.

RepositoryServiceProvider

Вместо внедрения BlogRepository в BlogController мы внедрим BlogRepositoryInterface и затем позволим сервис-контейнеру решать, какой репозиторий будет использоваться. Это можно сделать в методе boot в AppServiceProvider, но я предпочитаю сделать нового провайдера для этого, для сохранения чистоты кода.

Мы делаем нового провайдера по причине, что когда проект начнет расти, то код станет очень грязным.
Представьте проект с десятком моделей и у каждой свой репозиторий — AppServiceProvider станет нечитаемым.

Вот так выглядит наш RepositoryServiceProvider:

Обратите внимание, как легко можно поменять BlogRepository на другое хранилище.

Не забудьте добавить RepositoryServiceProvider в список провайдеров в файле config/app.php. После этого нужно очистить кеш конфигурации еще раз.

Вот и все!

Вы успешно использовали паттерн Репозиторий. Было не так уж и сложно, верно?

Следите за выходом новых статей через наши каналы в Телеграм и Вконтакте

Как не надо использовать паттерн «Repository»

Обобщенный паттерн Репозиторий

Чтобы избежать недоразумений, давайте для начала проясним, что здесь имеется ввиду под понятием обобщенного репозитория. Видели ли вы когда-нибудь интерфейс вроде этого:

Или может вы видели его брата-близнеца, который имеет иной вариант метода Get:

Вдохновение для написания первого примера пришло из официальной документации Microsoft для ASP.NET MVC 4. Что же касательно второго примера, в Интернете вы можете найти бесконечное число блогов, описывающих именно этот вариант написания репозитория — иногда лишь с небольшими отличиями в виде возвращения IEnumerable вместо IQueryable . И в последнем случае часто еще и с дополнительным методом для генерации запросов. Что-то вроде этого:

Так что плохого в них, вы можете спросить? Почти ничего, если не считать плохое именование методов из примера Microsoft (здесь лучше было бы использовать Find и FindAll вместо Get и GetAll).

Но «почти ничего» — это далеко не то же самое, что «ничего». Первую проблему, которую я здесь вижу, так это нарушение Принципа Сегрегации Интерфейсов (Interface Segregation Principle). Они выражают полный набор CRUD-операций даже для тех сущностей, для которых операции удаления не имеют никакого смысла (к примеру, когда вы деактивируете пользователей вместо удаления их записей из базы данных). Но эта проблема может быть решена при помощи просто разбиения на три интерфейса — для чтения, обновления и удаления записей.

Реальная проблема, которую мы здесь имеем, заключается в некорректном использовании. Оригинальная идея заключается в том, что интерфейсы должны быть использованы в качестве базовых для ваших пользовательских интерфейсов. Что-то вроде этого:

Ваши обработчики событий и сервисы (клиенты пользовательских репозиториев) должны сами определять нужные методы и помещать их в пользовательские интерфейсы.

Это в теории. К сожалению, за свою карьеру мне довольно часто приходилось видеть несколько иную картину:

Кто-то создал рабочую реализацию IGenericRepository. Что хуже в этой реализации, так это то, что эта имплементация почти всегда регистрируется в IoC-контейнере и может быть внедрена в ваши обработчики команд и сервисы точно так же, как и любая другая зависимость.

Код выглядит достаточно красиво и чисто. На самом деле — нет. Почему именно так — скажу немного позже. Теперь же я бы хотел поговорить об одном из решений неправильной интерпретации GenericRepository многих разработчиков. Само решение выглядит примерно так (диалог на code-review):

Джим Сеньор: вы когда-либо слышали, что NHibernate ISession или DbSet из Entity Framework на самом деле репозитории? А то, что вы только что создали — всего лишь небольшая обертка над ISession или DbSet? Де-факто, мы можем отказаться от этого обобщенного репозитория и просто использовать DbSet — результат будет аналогичным. Единственное, в чем IGenericRepository действительно полезен — так это в том, что он скрывает большую часть ненужных для нас методов, которыми обладает DbSet.

Джонни Джуниор: да, в этом действительно есть смысл. Я полагаю, использование обобщенного репозитория в нашем случае было лишним (счастливо возвращаемся к написанию программы…)

Читать еще:  Phproxy remove client

Как по мне, что GenericRepository , что DbSet – в большинстве ситуаций их использование лишнее (ну разве что вы пишите наиболее заCRUDженное приложение в своей жизни). Почему? Вот почему:

  • Единственный способ убедиться, что все LINQ-запросы будут верно преобразованы в SQL — это протестировать их на том же типе базы данных, что находятся в продакшине. Кроме того, в таком случае задача написания интеграционных тестов становится достаточно затруднительной. К примеру, взглянем на код:

Чтобы выполнить эти запросы, должны быть соблюдены два условия. За счет этого интеграционный тест становится менее читабельным. Теперь же давайте представим, что этот метод помещен в метод репозитория. Теперь в тесте нам нужно просто вызвать метод репозитория и проверить результат — просто, не так ли?

  • Я уверен, вы согласитесь, что LINQ-предикаты внутри сервисов нельзя использовать повторно и они имеют плохую тенденцию повторяться на протяжении всего кода. Даже если программист желает извлечь запрос в свой собственный метод, обычно это будет приватный метод конкретного сервиса. Инкапсуляция запросов внутри методов репозитория приведет к большей читабельности кода и в значительной мере упростить его использование.
  • LINQ-предикаты не именуются. Как правило, единственный способ узнать, что же, собственно говоря, делает наш запрос (без углубления в его логику), так это имя переменной-результата. К сожалению, искусство информативного именования переменных постигается с опытом, а поскольку в нашей индустрии полно начинающих программистов, мы имеем дело с переменными вроде result, ordersToProcess или просто — orders. Создавая обертку над запросами в виде метода репозитория, это автоматически обязывает разработчика дать этому самому методу конкретное имя. Даже если это имя не из лучших, в последствии провести небольшой рефакторинг не составит труда!
  • Порой, по причине скорости, мы вынуждены доставать данные из базы посредством чистого SQL. Подумайте, вы действительно хотите в своей бизнес-логике работать с такими низкоуровневыми сущностями, как DbConnection или SqlException? Подобный код лучше прятать за методами репозиториями.

Никаких обобщений — только конкретные репозитории

Перед тем как начать разработку репозитория, необходимо определить его интерфейс. Интерфейс должен содержать только те методы, которые нужны конечным клиентам. Другими словами, если никому не нужно удалять сущности данного типа, нет никакого смысла в написании функционала для их удаления.

Если вы боитесь закончить среди разных имен для базовых CRUD-операций — таких как Delete в одном репозитории и Remove в другом — вы можете создать полезные интерфейсы типа ICanDeleteEntity , ICanUpdateEntity и так далее. В таком случае конкретный репозиторий будет реализовывать только действительно нужные из них.

Ни один из методов репозитория не должен возвращать IQueryable . Также убедитесь, что репозиторий не возвращает IQueryable , скрытые за IEnumerable . Всегда вызывайте ToList() или ToArray() дабы «материализовать» значения, придав им более конкретный тип данных.

Как только заходит речь о реализации репозитория, вы можете наследовать абстрактный класс GenericRepository . Если угодно — можно также использовать напрямую ISession и DbSet. Не важно, какой подход вы выбираете. Помните: любые «лишние» в данном контексте методы — по типу Delete из базового класса — будут скрыты за интерфейсом репозитория.

Также не забывайте, что ваш репозиторий не несет ответственности за транзакции базы данных. Лучше всего эта концепция реализуется при помощи паттерна Unit Of Work. Этот паттерн уже реализован в рамках ISession и DatabaseContext. Все, что нам нужно — это хороший интерфейс над ними:

Для большинства веб-приложений чтобы начать транзакцию через IUnitOfWork в виде начала запроса и Commit / Rollback в конце запроса — этого функционала вполне достаточно. Так же подобное может быть достигнуто при помощи фильтров действий или декораторов обработчиков / сервисов.

Пример репозитория, созданного при соблюдении всех приведенных рекомендаций:

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

Немного о недостатках

Не существует роз без шипов, и представленный выше подход — не исключение. Некоторые из них можно решить, используя отличимую от трехуровневой архитектуру. Наиболее часто с конкретными репозиториями возникали следующие проблемы:

  • За длительное время репозитории могут набрать большое количество Find-методов. Достаточно часто эти методы похожи друг на друга. Есть два способа это предотвратить. Один из них — это группировка нескольких Find-методов в один общий. Этот метод должен принимать object, что представляет собой критерий запроса. Пример:

Чтобы создать запрос с критерием запроса, мы строим набор критериев шаг за шагом:

Зачастую слишком большие репозитории обрастают целой плеядой различных сервисов. Вы можете избежать этого при помощи чего-то, что я называю CQRS-light. Отличием от полной версии является использование тех же таблиц как для чтения, так и для записи. Используя эту технологию, мы можем использовать ту же самую ORM как для чтения, так и для записи. Обращаться же к CQRS только в тех случаях, когда приложение в этом действительно нуждается (например, при обработке 80+ колонок, с использованием 20+ join). Диаграмма ниже представляет типичную архитектуру CQRS-приложения:

Ключевые принципы CQRS-light следующие:

  • Деление всех действий пользователя на две категории. Первая категория — действия, изменяющие систему. К примеру, оформление заказа в интернет-магазине. Вторая категория — действия, систему не трогающие. К примеру, просмотр списка товаров. Первая категория пишет — это команды, другая — читает, это запросы.
  • Обработчики запросов для доступа к данным могут использовать только репозитории. Доступ к данным может осуществяться посредством любой технологии. Обычная конфигурация включает в себя ORM для чтения-записи, ORM для записи и микро-ORM для чтения (вроде Dapper) или же ORM для записи и чистый SQL для чтения.
  • Обработчики команд для доступа и изменения данных могут использовать только репозитории. Для получения данных из базы обработчики команд не должны использовать обработчики запросов. Если обработчик команд должен выполнить комплексный запрос и этот запрос должен получить ответ от обработчика запроса, вы должны продублировать логику этого обработчика запроса и поместить его как в обработчик запроса, так и в метод репозитория (операции чтения и записи должны быть разделены).
  • Обработчики запросов тестируются только в интеграционных тестах. Для обработчиков команд существуют юнит и — опционально — также интеграционные тесты.

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

Теперь же давайте вернемся к теме недостатков паттерна конкретного репозитория. Другой недостаток, о котором я бы хотел упомянуть, заключается в нежелательной миграции бизнес-логики в определение запросов. К примеру, даже такой простой запрос:

содержит часть бизнес-логики, описывающей необходимые параметры, дабы заказ стал активным. Как правило, ORM не позволяет нам инкапсулировать такие части логики в отдельные свойства, как IsActive.

Что нам здесь нужно, так это паттерн спецификации. Теперь наш запрос в форме паттерна спецификации будет выглядеть так:

Паттерн «Репозиторий». Основы и разъяснения

Repository commonly refers to a storage location, often for safety or preservation.
— Wikipedia

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

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

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

Читать еще:  Php try catch finally

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

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

Хорошим способом понять как работают репозитории является представление вашего приложения постоянно работающим, в этом случае все объекты остаются в памяти. Вероятность критических сбоев и реакцию на них в этом эксперименте можно пренебречь. Представьте, что у вас есть Singleton-экземпляр репозитория для сущностей Member , MemberRepository .

Затем создайте новый объект Member и добавьте его в репозиторий. Позже, вы запросите у репозитория все элементы, хранящиеся в нем, таким образом вы получите коллекцию, которая содержит этот объект внутри. Возможно вы захотите получить какой-то конкретный объект по его ID, это также возможно. Очень легко представить себе, что внутри репозитория эти объекты хранятся в массиве или, что еще лучше, в объекте-коллекции.

Проще говоря, репозиторий — это особый вид надежных коллекций, которые вы будете использовать снова и снова, чтобы хранить и фильтровать сущности.

Взаимодействие с Репозиторием

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

Теперь мы можем получить доступ к объекту позже. Примерно так:

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

Должны ли репозитории создавать сущности?

Вы можете встретить такие примеры:

Я видел множество аргументов приводящихся в пользу этого, но совершенно не заинтересован в подобном подходе.

Прежде всего, репозитории — это коллекции. Я не уверен в том, зачем коллекция должна быть коллекцией и фабрикой. Я слышал аргументы вроде «если обращаться удобнее так, то почему бы не повесить обработчик на подобные действия»?

На мой взгляд, это анти-паттерн. Почему бы не позволить классу Member , иметь свое собственное понимание как и зачем создается объект или почему бы не сделать отдельную фабрику для создания более сложных объектов?

Если мы относимся к нашим репозиториям как к простым коллекциям, так значит и не нужно нагружать их лишним функционалом. Я не хочу классов коллекций, которые ведут себя как фабрики.

В чем выгода использования репозиториев?

Основное преимущество репозиториев — это абстрактный механизм хранения для коллекций сущностей.

Предоставляя интерфейс MemberRepository мы развязываем руки разработчику, который уже сам решит как и где хранить данные.

Таким образом, большинство наших приложений знает только абстрактное понятие MemberRepository и его использование может быть отделено от фактической реализации. Это очень раскрепощает.

К чему относятся репозитории: Domain или Application Service Layer?

Итак, вот интересный вопрос. Во-первых, давайте определим, что Application Service Layer — это многоуровневая архитектура, которая отвечает за специфические детали реализации приложения, такие как целостность базы данных, и различные реализации работы с интернет-протоколами (отправка электронной почты, API) и др.

Определим термин Domain Layer как слой многоуровневой архитектуры, которая отвечает за бизнес-правила и бизнес-логику.

Куда же попадет репозиторий при таком подходе?

Давайте посмотрим на нашем примере. Вот код, написанный ранее.

В этом примере я вижу много деталей реализации. Они, несомненно, должны входить в слой приложения

А теперь давайте удалим все детали реализации из этого класса…

Хм… это начинает выглядеть знакомо… Что же мы забыли?

Возможно, получившийся код напоминает вам это?

Это означает, что интерфейс находится на границе слоев. и на самом деле может содержать доменно-специфические концепты, но сама реализация не должна этого делать.

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

Свобода смены хранилищ данных

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

По-моему, это не совсем правда… я бы даже сказал, что это очень плохой аргумент. Самой большой проблемой объяснения концепции репозиториев является то, что сразу напрашивается вопрос «вы действительно хотите это делать?». Я НЕ хочу чтобы подобные вопросы влияли на использование паттерна репозитория.

Любое достаточно хорошо спроектированное объектно-ориентированное приложение автоматически подходит под приведенное преемущество. Центральной концепцией ООП является инкапсуляция. Вы можете предоставить доступ к API и скрыть реализацию.

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

Тестирование при использовании паттерна «Репозиторий»

Ну, тут все просто. Давайте предположим, что у вас есть объект, который обрабатывает что-то вроде регистрации участников…

Во время очередной операции, я могу взять экземпляр DoctrineMemberRepository . Однако, во время тестирования легко можно заменить его на экземпляр ArrayMemberRepository. Они оба реализуют один и тот же интерфейс.

Упрощенный пример теста может выглядеть примерно так…

В этом примере мы тестируем обработчик. Нам не нужно проверять корректность хранения данных репозитория в БД (или еще где). Мы тестируем конкретное поведение этого объекта: регистрируем пользователя на основе данных формы, а затем передаем их в репозиторий.

Коллекция или Состояние

В книге Implementing Domain-Driven Design Vaughn Vernon делает различие между типами репозиториев. Идея коллекцио-ориентированного репозитория (ориг. — collection-oriented repository) в том, что работа с репозиторием идет в памяти, как с массивом. Репозиторий, ориентированный на хранение состояний (ориг. — persistence-oriented repository) содержит в себе идею, что в нем будет какая-то более глубокая и продуманная система хранения. По сути различия лишь в названиях.

Замечу, что это лишь мое мнение и пока что я придерживаюсь именно его в вопросах использования репозиториев. Однако, хотел бы предупредить, что возможно могу передумать. В конце-концов, я сосредотачиваюсь на них как на коллекциях объектов с теми же обязанностями, что и у любого другого объекта-коллекции.

Дополнительная информация

everzet создал проект на Github о репозиториях на который, безусловно, стоит посмотреть. Внутри вы найдете примеры работы с хранением в памяти и файлах.

Итоги

Я считаю, что…

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

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

Если у вас есть вопросы или если ваше мнение отличается от моего, пожалуйста, пишите комментарии ниже.

Как всегда, я намерен обновлять статью, чтобы синхронизировать ее с моим текущим мнением.

Реализация паттерна “Репозиторий”

Никак не могу найти толковую документацию по данному паттерну. Нужен хотя бы один полный пример как его реализовать.

Читать еще:  Php select from

1 ответ 1

Взял описание из одного приведенного ниже источника:

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

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

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

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

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

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

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

Затем вы создаёте нового Member и добавляете его в репозиторий. Позже вы запрашиваете у репозитория всех members и получаете назад коллекцию, которая содержит Member которого вы добавили. Возможно вы захотите получить отдельного Member по ID, вы можете сделать это тоже. Легко представить что внутри репозитория эти объекты Member хранятся как массив или лучше как коллекция объектов.

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

Взаимодействие с репозиторием

Представьте что вы создали сущность Member. Вы удовлетворены объектом Member(Участник), затем когда запрос заканчивается, объект Member(Участник) исчезает. Затем участник пытается авторизироваться в вашем приложении и не может. Очевидно что мы должны сделать Member(Участника) доступным в других частях нашего приложения.

Затем мы захотели получить Member(Участника) позже, например так:

Теперь мы можем хранить объекты Member в одной части нашего приложения, и затем получать их в другой части.

Должен ли репозиторий создавать сущности?

Вы могли делать что-то вроде этого:

Я видел людей которые приводили аргументы для этого подхода. Но я крайне не рекомендую его.

Ещё раз, репозитории это коллекции. Я не уверен что коллекция должна быть ещё и фабрикой. Я слышал аргументы типа.. они хранят состояние, почему же не хранить ещё и создание сущностей?

В моём разуме это анти-паттерн. Почему не разрешить Member(Участнику) иметь свои собственные представления о своём создании, или почему не иметь фабрику которая специально разработана для обеспечения создания более комплексных объектов.

Если мы используем наши репозитории как простые коллекции, тогда мы даём им одну ответственность. Я не хочу классы коллекции которые так же являются фабриками.

В чём преимущество репозиториев?

Главное преимущество репозиториев — это абстрактный механизм хранилища для управляющей коллекции сущностей.

Когда мы создаём интерфейс MemberRepository, мы разрешаем существование любого числа его конкретных реализаций:

Если идти по этому пути, то наше приложения знает только абстрактную концепцию MemberRepository и наше использование этого репозитория может быть разделено на несколько реализаций. Это вполне гибко.

Репозитории принадлежат к слою домена или слою приложения?

Это достаточно интересный вопрос. Во-первых, давайте определим слой приложения как многослойную архитектуру, которая ответственна за реализацию конкретных деталей работы приложения, таких как работа с БД, знания о протоколе передачи данных(отправка email, взаимодейтвие с API) и др.

Давайте определим слой домена как многослойную архитектуру, которая ответственна за хранение бизнес-правил и бизнес-логики.

Работая с этими определениями, в которое из них подходит наш репозиторий?

Давайте посмотрим на наш пример из кода выше:

В этом примере я вижу множество деталей реализации. Эти детали реализации безусловно относятся к слою приложения.

Давайте уберём все детали реализации из этого класса.

Хм, это кажется достаточно знакомым. Где же это было?

Может быть это напоминает вам это?

Это значит что интерфейс находится на границе слоёв. Сам интерфейс может содержать специфичные для домена концепции, но реализация интерфейса не должна.

Интерфейс репозитория принадлежит к слою домена. Реализация интерфейса принадлежит к слою приложения. Это значит что мы можем писать подсказки к нашим репозиториям в слое домена даже не дотрагиваясь к слою приложения.

Свобода выбора хранилища данных

Вы наверное слышали в разговорах о концепциях объектно-ориентированного программирования что-то типа такого — «и вы полностью неограничены в смене одного хранилища данных на другое позже».

Я пришёл к выводу что это не совсем правда.. это очень слабый аргумент. Самая большая проблема с этим объяснением, это то, что оно приводит к вопросу — «А вы действительно захотите менять хранилище данных?». Я не хочу чтобы ответ на этот вопрос определял использовать или нет паттерн репозиторий.

Любое хорошо спроектированное объектно-ориентированное приложения автоматически идёт с этим типом преимущества. Центральная концепция объектной-ориентированности это инкапсуляция. Вы можете показать API и скрыть реализацию.

Правда в том, что вы наверное не захотите менять одну ORM на другую. Но у вас хотя бы будет хороший способ реализовать это. Тем не менее, смена реализаций репозиториев отлично подходит для тестирования.

Тестируемость с паттерном репозиторий

Угадайте что? Это очень вкусно. Допустим у вас есть объект, который содержит что-либо похожее на регистрацию участников:

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

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

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

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

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

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

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

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

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

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

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

Ссылка на основную публикацию
Adblock
detector