На мінулым тыдні я напісаў
<?php
interface PostRepository
{
public function getById($id): Post;
public function save(Post $post);
public function delete($id);
}
Аднак, у рэальных праектах, калі рэпазітары такі было вырашана выкарыстоўваць, у іх часта дадаюцца метады для выбарак запісаў:
<?php
interface PostRepository
{
public function getById($id): Post;
public function save(Post $post);
public function delete($id);
public function getLastPosts();
public function getTopPosts();
public function getUserPosts($userId);
}
Гэтыя метады можна было б рэалізаваць праз Eloquent scopes, але перагружаць класы сутнасцяў абавязкамі па выбарцы саміх сябе – не самая лепшая задума і вынас гэтага абавязку ў класы рэпазітараў выглядае лагічным. Ці так гэта? Я спецыяльна візуальна падзяліў гэты інтэрфейс на дзве часткі. Першая частка метадаў будзе скарыстана ў аперацыях запісу.
Стандартная аперацыі запісы гэта:
- канструяванне новага аб'екта і выклік PostRepository::save
- PostRepository::getById, маніпуляцыі з сутнасцю і выклік PostRepository::save
- выклік PostRepository::delete
У аперацыях запісу няма выкарыстання метадаў выбаркі. У аперацыях чытання ж выкарыстоўваюцца толькі метады get*. Калі пачытаць пра Interface Segregation Principle (літара I в Цвёрдыя), то стане зразумела, што наш інтэрфейс атрымаўся занадта вялікім і выконваючым як мінімум два розныя абавязкі. Час дзяліць яго на два. Метад getById неабходны ў абодвух, аднак пры ўскладненні дадатку яго рэалізацыі будуць рознымі. Гэта мы ўбачым крыху пазней. Пра бескарыснасць write-часткі я пісаў у мінулым артыкуле, таму ў гэтым я пра яе проста забудуся.
Read жа частка мне здаецца не такой ужо і бескарыснай, бо нават для Eloquent тут можа быць некалькі рэалізацый. Як назваць клас? Можна ReadPostRepository, але да шаблону ёмішча ён ужо мае малое дачыненне. Можна проста PostQueries:
<?php
interface PostQueries
{
public function getById($id): Post;
public function getLastPosts();
public function getTopPosts();
public function getUserPosts($userId);
}
Яго рэалізацыя з дапамогай Eloquent даволі простая:
<?php
final class EloquentPostQueries implements PostQueries
{
public function getById($id): Post
{
return Post::findOrFail($id);
}
/**
* @return Post[] | Collection
*/
public function getLastPosts()
{
return Post::orderBy('created_at', 'desc')
->limit(/*some limit*/)
->get();
}
/**
* @return Post[] | Collection
*/
public function getTopPosts()
{
return Post::orderBy('rating', 'desc')
->limit(/*some limit*/)
->get();
}
/**
* @param int $userId
* @return Post[] | Collection
*/
public function getUserPosts($userId)
{
return Post::whereUserId($userId)
->orderBy('created_at', 'desc')
->get();
}
}
Інтэрфейс павінен быць звязаны з рэалізацыяй, напрыклад у AppServiceProvider:
<?php
final class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(PostQueries::class,
EloquentPostQueries::class);
}
}
Дадзены клас ужо карысны. Ён рэалізуе сваю адказнасць, разгрузіўшы гэтым альбо кантролеры, альбо клас сутнасці. У кантролеры ён можа быць скарыстаны так:
<?php
final class PostsController extends Controller
{
public function lastPosts(PostQueries $postQueries)
{
return view('posts.last', [
'posts' => $postQueries->getLastPosts(),
]);
}
}
метад PostsController::lastPosts проста просіць сабе якую-небудзь рэалізацыю PostsQueries і працуе з ёй. У правайдэры мы звязалі PostQueries з класам EloquentPostQueries і ў кантролер будзе падстаўлены гэты клас.
Давайце ўявім, што наша дадатак стала вельмі папулярным. Тысячы карыстальнікаў у хвіліну адкрываюць старонку з апошнімі публікацыямі. Найбольш папулярныя публікацыі таксама чытаюцца вельмі часта. Базы дадзеных не вельмі добра спраўляюцца з такімі нагрузкамі, таму выкарыстоўваюць стандартнае рашэнне - кэш. Акрамя базы дадзеных, нейкі злепак дадзеных захоўваецца ў сховішчы аптымізаваным да вызначаных аперацый memcached або Redis.
Логіка кэшавання звычайна не такая складаная, але рэалізоўваць яе ў EloquentPostQueries не вельмі правільна (хоць бы з-за Прынцып адзінай адказнасці). Нашмат больш натуральна выкарыстоўваць шаблон Дэкаратар і рэалізаваць кэшаванне як дэкарыравання галоўнага дзеяння:
<?php
use IlluminateContractsCacheRepository;
final class CachedPostQueries implements PostQueries
{
const LASTS_DURATION = 10;
/** @var PostQueries */
private $base;
/** @var Repository */
private $cache;
public function __construct(
PostQueries $base, Repository $cache)
{
$this->base = $base;
$this->cache = $cache;
}
/**
* @return Post[] | Collection
*/
public function getLastPosts()
{
return $this->cache->remember('last_posts',
self::LASTS_DURATION,
function(){
return $this->base->getLastPosts();
});
}
// другие методы практически такие же
}
Не звяртайце ўвагі на інтэрфейс ёмішча у канструктары. Па незразумелай прычыне так вырашылі назваць інтэрфейс для кэшавання ў Laravel.
Клас CachedPostQueries рэалізуе толькі кэшаванне. $this->cache->remember правярае ці няма дадзенага запісу ў кэшы і калі няма, тое выклікае callback і запісвае ў кэш якое вярнулася значэнне. Засталося толькі ўкараніць дадзены клас у дадатак. Нам неабходна, каб усе класы, якія ў дадатку просяць рэалізацыю інтэрфейсу PostQueries сталі атрымліваць асобнік класа CachedPostQueries. Аднак сам CachedPostQueries у якасці параметру ў канструктар павінен атрымаць клас EloquentPostQueries, паколькі ён не можа працаваць без "сапраўднай" рэалізацыі. Змяняем AppServiceProvider:
<?php
final class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(PostQueries::class,
CachedPostQueries::class);
$this->app->when(CachedPostQueries::class)
->needs(PostQueries::class)
->give(EloquentPostQueries::class);
}
}
Усе мае пажаданні даволі натуральна апісваюцца ў правайдэры. Такім чынам, мы рэалізавалі кэшаванне для нашых запытаў толькі напісаўшы адзін клас і памяняўшы канфігурацыю кантэйнера. Код астатняга прыкладання не памяняўся.
Зразумела, для паўнавартаснай рэалізацыі кэшавання неабходна яшчэ рэалізаваць інвалідацыю, каб выдалены артыкул не вісеў на сайце яшчэ нейкі час а выдаліўся адразу. Але гэта ўжо дробязі.
Вынік: мы выкарыстоўвалі не адзін, а цэлых два шаблоны. Шаблон Command Query Responsibility Segregation (CQRS) прапануе цалкам падзяліць аперацыі чытання і запісы на ўзроўні інтэрфейсаў. Я прыйшоў да яго праз Interface Segregation Principle, што кажа аб тым, што я ўмела маніпулюю шаблонамі і прынцыпамі і выводжу адзін з другога як тэарэму 🙂 Зразумела, не кожнаму праекту неабходна такая абстракцыя на выбаркі сутнасцяў, але я падзялюся з вамі фокусам.На пачатковым этапе распрацоўкі прыкладання можна проста стварыць клас PostQueries са звычайнай рэалізацыяй праз Eloquent:
<?php
final class PostQueries
{
public function getById($id): Post
{
return Post::findOrFail($id);
}
// другие методы
}
Калі ўзнікне неабходнасць у кэшаванні, лёгкім рухам можна стварыць інтэрфейс (або абстрактны клас) на месцы гэтага класа PostQueries, яго рэалізацыю скапіяваць у клас EloquentPostQueries і перайсці да схемы, апісанай мною раней. Астатні код прыкладання мяняць не трэба.
Усе гэтыя фокусы з класамі, інтэрфейсамі, Ін'екцыя залежнасці и CQRS падрабязна апісаны ў
Крыніца: habr.com