В этой статье я попытаюсь объяснить, как использовать шаблон репозитория в Laravel framework и почему он полезен.
Давайте начнем с простого примера. Вот как выглядит типичный контроллер Laravel:
<?php
class PostsController extends Controller {
public function index() {
$posts = Post::all();
return View::make('posts.index', compact('posts'));
}
}
В большинстве случаев это выглядит хорошо, но когда приложение растет и такие вещи, как тестируемость и масштабируемость, становятся важными, приведенный выше код не совсем удовлетворяет.
Проблемы
Итак, какие проблемы есть в коде выше?
1. Связь, сцепление (Coupling)
Прежде всего, приведенный выше код связывает контроллер с моделью Post.
Как правило, тесная связь означает, что одно изменение в одном модуле может привести к изменениям в других модулях, поэтому, прежде чем делать какие-либо изменения, разработчик должен понять всю систему.
Тесно связанные приложения не являются гибкими и в большинстве случаев стоимость изменений намного выше, чем стоимость разработки.
Только представьте себе, как трудно будет перенести ваше приложение из MySQL в другую систему управления базами данных, если такие зависимости есть в каждом отдельном контроллере.
С заменой базы данных в проекте на столько редкий кейс, что я его ни разу за 20 лет не встречал. Лучше привести пример с сервисом кеша или логирования. Кеш может быть в редисе, в файлах, в памяти. Логирование может быть в файлы, в слаке, в грейлоге и т.п.
прим. автора блога
2. Жестко закодированный запрос к базе данных
Post::all()
отлично работает, когда в нашей базе данных есть только 10 или 20 записей. Но что произойдет, когда компания станет популярной и будет публиковать десятки постов каждый день? В очень короткое время у нас будут сотни или даже тысячи сообщений в блогах, и это замедлит работу нашего приложения.
Чтобы избежать этого, нам придется переписать код и использовать что-то вроде этого:
Post::take($x)->skip($y)->get();
Выглядит просто, но помните ли вы, что модели многоразовые? Если мы уже использовали жестко запрограммированный Post::all()
запрос в нескольких местах, все они должны быть переписаны вручную. Это всего лишь простой пример, а в реальном проекте все может быть гораздо сложнее. Конечно, это нарушает DRY (не повторяйся) принцип.
3. Жирные контроллеры
Как я уже упоминал выше, в реальных проектах мы почти не пользуемся Post::all()
. На самом деле, скорее всего, код выглядит примерно так же, как следующий фрагмент кода:
if ($somethingHappened) {
$posts = Post::recent()
->orderBy('created_at', 'desc')
->take($x)
->skip($y)
->blah()->blah()->blah()
->get();
} else {
$posts = Post::whereNothingHappened(1)
->orderBy('created_at', 'desc')
->take($x)
->skip($y)
->bleh()->bleh()->bleh()
->get();
}
Это раздувает наши контроллеры, и снова нам придется повторять весь этот код в разных местах, чтобы получить список сообщений.
4. Тестируемость
Хорошие программисты пишут тесты, и большинство из них имеют привычку спрашивать себя: “как это проверить?”.
Эта статья не о тестировании, поэтому я не хочу погружаться слишком глубоко, но код, который я показал выше, нелегко проверить. На самом деле это можно сделать, включив такие вещи, как Mock, но, с моей точки зрения, всегда лучше избегать дополнительной сложности.
И это действительно возможно.
Решение проблемы
Как вы уже, наверное, догадались, шаблон репозитория помогает нам решить все вышеперечисленные проблемы.
Типичный репозиторий выглядит следующим образом:
<?php namespace Example\Repositories;
class PostRepository {
public function getPosts() {
return Post::all();
}
}
И контроллер теперь выглядит так:
<?php
use Example\Repositories\PostRepository;
class PostsController extends Controller {
public $repository;
public function __construct(PostRepository $repository) {
$this->repository = $repository;
}
public function index() {
$posts = $this->repository->getPosts();
return View::make('posts.index', compact('posts'));
}
}
Обратите внимание, что Laravel достаточно умен, чтобы автоматически заинжектить экземпляр PostRepository
в контроллер
Простая оболочка вокруг нашей модели помогает нам повысить тестируемость, потому что теперь мы можем внедрить репозиторий в контроллер. Этот метод называется инъекцией зависимостей, и с помощью этого подхода мы можем протестировать класс с помощью дополнительного инструмента.
Теперь наши контроллеры находятся в гораздо лучшей форме. Нет необходимости писать в них сложные Eloquent запросы, потому что мы можем переместить их в наш репозиторий.
Нам больше не нужно повторяться. Когда логика меняется, мы просто меняем код в репозитории.
Но этот код все еще не совершенен. Представьте себе, что нам нужно будет изменить уровень базы данных с MySQL, скажем, на MongoDB. Текущий код намного лучше первоначального, но должны ли мы заменить весь код MySQL в репозиториях совершенно новым кодом MongoDB? Мы можем, но есть лучший вариант: мы можем создать отдельный репозиторий для MongoDB и переключаться между реализациями столько раз, сколько захотим.
Инверсия зависимостей
В приведенном выше коде мы снова связываем наш контроллер. На этот раз с конкретным хранилищем. Это означает, что если мы создадим отдельный репозиторий для MongoDB, то нам потребуется изменить следующую строку в каждом отдельном месте, где мы используем репозиторий:
use Example\Repositories\PostRepository;
Чтобы избежать этого, давайте создадим контракт или, как мы называем его в PHP, интерфейс:
<?php namespace Example\Repositories;
interface PostRepositoryInterface {
public function getPosts();
}
Теперь каждый отдельный репозиторий post должен реализовать этот интерфейс:
<?php namespace Example\Repositories;
class MongoPostRepository implements PostRepositoryInterface {
public function getPosts() {
//mongo specific implementation
}
}
Контроллер теперь должен выглядеть так:
<?php
use Example\Repositories\PostRepositoryInterface;
class PostsController extends Controller {
public $repository;
public function __construct(PostRepositoryInterface $repository) {
$this->repository = $repository;
}
public function index() {
$posts = $this->repository->getPosts();
return View::make('posts.index', compact('posts'));
}
}
Laravel достаточно умен, чтобы заинжектить экземпляр PostRepository в прошлый раз, и он достаточно умен, чтобы попытаться снова это сделать, но с использованием PostRepositoryInterface. Но нам необходимо будет ему помочь понять, какая реализация у нашего интерфейса. Для этого в сервис провайдере мы укажем:
<?php namespace Example;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
use Example\Repositories\MongoPostRepository;
class RepositoryServiceProvider extends BaseServiceProvider
{
public function register()
{
$this->app->bind(
'Example\Repositories\PostRepositoryInterface',
'Example\Repositories\MongoPostRepository'
);
}
}
Этот код в основном говорит, что”всякий раз, когда я запрашиваю экземпляр PostRepositoryInterface, дайте мне экземпляр MongoPostRepository”.
Как только мы зарегистрируем этого поставщика услуг в app.php
конфигурации, Laravel сможет создать его экземпляр PostRepositoryInterface
. Если мы передумаем и захотим вернуться к реализации MySQL, нам просто придется заменить одну строку кода в поставщике услуг.
Вывод
Будучи довольно простым в реализации, шаблон репозитория дает огромное преимущество. Большинство людей думают, что им никогда не понадобится такая гибкость, они не планируют писать тесты для своего небольшого проекта, но это плохой подход.
Никогда не знаешь, когда проект станет больше и когда он наберет большую популярность. Небольшое усилие по внедрению репозиториев гарантирует, что рост и рост популярности проекта могут быть более плавными и менее болезненными.
Надеюсь, вам понравилась статья, и она была полезной.