Карысныя рэпазітары з Eloquent?

На мінулым тыдні я напісаў артыкул аб бескарыснасці шаблону Рэпазітар для Eloquent сутнасцяў, аднак паабяцаў расказаць як мага часткова яго выкарыстоўваць з карысцю. Для гэтага паспрабую прааналізаваць як звычайна выкарыстоўваюць гэты шаблон у праектах. Мінімальна неабходны набор метадаў для рэпазітара:

<?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 падрабязна апісаны ў маёй кнізе «Архітэктура складаных вэб прыкладанняў». Там жа разгадка загадкі чаму ўсе мае класы ў прыкладах да гэтага артыкула пазначаныя як final.

Крыніца: habr.com

Дадаць каментар