Что такое репозитории?
Если вы читали мои предыдущие посты, то вы, наверное, уже знаете, что из себя представляют репозитории.
Но понимаете ли вы, что является причинами для использования Репозиториев? Хотя для некоторых причины использования паттерна очевидны, я думаю, многие люди всё ещё ищут их.
Я считаю, что есть 4 базовых преимущества в использовании паттерна Репозиторий в приложении.
1. Хранилище данных как деталь приложения
Первое большое преимущество использования паттерна Репозиторий — это то, что он перемещает вас ближе к размышлению о базе данных как о всего-лишь детали приложения.
Многие приложения разрастаются при проектировании схемы базы данных. Хотя множество CRUD-ориентированных приложений очень ориентированы на базу данных, это неправильный подход для приложений другого типа.
База данных — это деталь вашего приложения. Вы должны проектировать ваше приложение, обходя знания о том, как будете хранить ваши данные.
Преимуществом использования паттерна Репозиторий в этом случае является то, что вы можете писать интерфейс Репозитория в начале проекта без реального размышления о технических деталях того, как будут храниться ваши данные.
Например, у вас может быть следующий интерфейс UserRepository
:
interface UserRepository {
public function findUserById($id);
public function findUserByUsername($username);
public function add(User $user);
public function remove($id);
}
Вместо того, чтобы беспокоиться об установке вашей базы данных, вы можете написать реализацию, основанную на хранилищах в памяти очень быстро:
class InMemoryUserRepository implements UserRepository {
/** @var */
private $users;
public function findUserById($id)
{
return $this->users[$id];
}
public function findUserByUsername($username)
{
return array_filter($this->users,
function (User $user) use ($username) {
return $user->username === $username;
});
}
public function add(User $user)
{
$this->users[$user->id] = $user;
}
public function remove($id)
{
unset($this->users[$id]);
}
}
Дальше вы можете продолжать строить действительно важные части вашего приложения, зная, что как только вам понадобится где-то хранить данные, вы просто сможете написать реализацию Репозитория, которая удовлетворяет требованиям вашего интерфейса Репозитория.
2. Намного проще тестировать
Ещё одно отличное преимущество, связанное с первым — использование паттерна Репозиторий делает тестирование вашего кода куда проще.
Когда вам нужно добавить или получить данные из базы данных в вашем приложении, вместо того, чтобы хардкодить эту зависимость, вы можете внедрить экземпляр объекта, удовлетворяющий требованиям вашего интерфейса Репозиторий.
Например, вы, наверное, не захотите писать следующий код в вашем приложении:
public function find($id)
{
$repository = new EloquentUserRepository;
return $repository->findUserById($id);
}
При создании нового экземпляра EloquentUserRepository
прямо в методе вы привязываете эту зависимость к вашему коду.
Вместо этого вы можете внедрить объект, который удовлетворяет требованиям интерфейса:
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
public function find($id)
{
return $this->repository->findUserById($id);
}
Внедрив этот объект, вы можете легко внедрять различные реализации в процессе тестирования, не требующие базу данных, например:
public function test_find_user()
{
$repository = new InMemoryUserRepository;
$controller = new UserController($repository);
$user = $controller->findUserById(1);
$this->assertInstanceOf('User', $user);
}
3. Односторонняя зависимость
Хорошие приложения складываются из нескольких различных слоёв, каждый из которых имеет свою сферу ответственности в приложении.
В самом верху слоя — пользовательский интерфейс. Он используется для отображения данных пользователю, принимая его данные и отправляя их в приложение.
Дальше у нас есть слой HTTP, который принимает пользовательские данные и направляет их туда, куда они должны попасть.
Затем в приложении идёт слой, который координирует и определяет, какой сервис нам нужен для удовлетворения запроса страницы.
После этого у нас есть слой домена, где находится бизнес-логика приложения.
И, наконец, в самом низу у нас есть база данных.
Как вы можете видеть, приложение складывается из различных слоёв. Каждый слой имеет свою сферу ответственности в приложении.
Каждый слой также фактически никак не зависит от слоя ниже. Пользовательский интерфейс не заботится о том, написано ли приложение на PHP, Ruby или Java. Так же, как и слой HTTP может посылать и принимать запросы, это всё что для него имеет важность.
Слой HTTP не знает ничего о том, как слой Приложения будет удовлетворять его запрос. Он заботится только об отправке соответствующего ответа.
Приложение не заботится о том, как слой Домена в соответствии с бизнес-правилами решает, что правильно, а что — нет, слой Приложения не знает понятия «бизнес-правила».
Слой Домена не заботится о том, как на самом деле хранятся данные, он заботится только об отправке и получении данных, чтобы удовлетворить запрос свыше.
Как вы можете увидеть, каждый слой ничего не знает о слое ниже.
Использование паттерна Репозиторий позволяет нам создавать одностороннюю зависимость между доменом и слоями хранения данных.
4. Иллюзия хранилища в памяти
Если мы посмотрим на определение Репозитория из книги «Patterns of Enterprise Application Architecture», то увидим следующую цитату:
Посредничество между доменом и хранилищем данных осуществляется посредством интерфейса-коллекции для доступа к объектам домена.
Одна из самых важных характеристик паттерна Репозиторий — это тот факт, что он предоставляет интерфейс коллекции.
Это значит, что вы можете думать о доступе к данным из вашей базы данных так же, как если бы работали с ними как со стандартным объектом-коллекцией.
Мы используем базы данных в наших приложениях, потому что нам нужен способ постоянного хранения данных.
Но мы должны мыслить о данных, как если бы они хранились в коллекциях, а не позволять терминологии баз данных влезать в наше приложение.
Это значит, что, например, вместо метода save(User $user)
у нас должен быть add(User $user)
.
Почему это важно? В конце концов, мы всё равно будем всё время использовать базы данных. И нельзя, чтобы база данных диктовала нам условия реализации или проектирования нашего приложения.
Путём моделирования взаимодействия с базой данных, как с коллекцией, мы отдаляемся от приложений, в центре которых стоит база данных, с которой мы работаем так долго.
Пишем первый репозиторий
Немного раньше мы ознакомились с паттерном Спецификация.
Паттерн Спецификация — это способ инкапсулирования бизнес-правил вокруг выборки объектов в нашем приложении. Для выборки этих объектов нам нужен способ обращения к базе данных.
В предыдущем руководстве мы внедрили интерфейс UserRepository
в наш объект спецификации.
Это хороший пример того, как мы не позволяем нашей базе данных владеть процессом, который важен для частей нашего приложения. Мы просто можем внедрить экземпляр интерфейса и позже не беспокоиться о базе данных.
Сегодня мы посмотрим на первые пробные шаги написания интерфейса UserRepository
.
Создадим файл Cribbb\Domain\Model\Identity
с названием UserRepository.php
:
<?php namespace Cribbb\Domain\Model\Identity;
interface UserRepository {}
Первые два метода, которые я добавлю, будут искать пользователя по E-Mail и username. Они нужны объекту спецификации:
/**
* Find a user by their email address
*
* @param Email $email
* @return User
*/
public function userOfEmail(Email $email);
/**
* Find a user by their username
*
* @param Username $username
* @return User
*/
public function userOfUsername(Username $username);
Следующий метод, который я создам, будет нужен для добавления пользователей в приложение. Мне он нужен будет тогда, когда я буду писать код для регистрации пользователей.
Как указано выше, я должен думать о Репозитории, как если бы была коллекция в памяти, а не база данных:
/**
* Add a new User
*
* @param User $user
* @return void
*/
public function add(User $user);
Как вы можете увидеть, я принимаю параметром экземпляр User. Репозиторий является ответственным за хранение и получение объектов. Репозиторий не является ответственным за получение простого массива атрибутов из запроса и создание объекта User.
И, наконец, я добавлю метод для возвращения следующего идентификатора. Если вы помните, на прошлой неделе я начал использовать UUID вместо автоинкрементных id.
Когда вы добавляете элемент в коллекцию, коллекция ответственна за то, чтобы предоставить следующий идентификатор, который может быть использован. И в этом случае за генерацию идентификатора отвечает не сама коллекция:
/**
* Return the next identity
*
* @return UserId
*/
public function nextIdentity();
Вы можете заметить, что я создал UserRepository
прямо в сердце слоя Identity
, который находится в слое домена нашего приложения.
UserRepository
— это часть бизнес-логики приложения. Тем не менее, реальная реализация этого Репозитория относится к инфраструктуре.
Поэтому, когда я пишу реализацию UserRepository
, я помещу её под пространством имён внутри инфраструктуры:
<?php namespace Cribbb\Infrastructure\Repositories;
use Doctrine\ORM\EntityRepository;
use Cribbb\Domain\Model\Identity\UserRepository;
class UserDoctrineORMRepository extends EntityRepository implements UserRepository {}
Заключение
Репозитории важны не только на техническом уровне, но и на уровне концептуального размышления о различных слоях приложения.
Использование Репозиториев в нашем приложении имеет множество преимуществ.
Во-первых, они предотвращают вас от попадания в болото технических деталей инфраструктуры проекта.
Во-вторых, они делают проще тестирование различных компонентов, взаимодействующих с вашей базой данных.
В-третьих, они предоставляют одностороннюю зависимость между слоями, предотвращая их размытость.
И наконец, они предоставляют иллюзию коллекции в памяти, так что терминология постоянных хранилищ не вкрадывается в язык нашего приложения.
Для меня работа с репозиториями делает аспект взаимодействия с хранилищем в приложении намного проще.