Корисна спремишта са Елокуент-ом?

Прошле недеље сам писао чланак о бескорисности шаблона Репозиторија за Елоквентне ентитете, међутим, обећао је да ће ми рећи како да то делимично искористим у своју корист. Да бих то урадио, покушаћу да анализирам како се овај шаблон обично користи у пројектима. Минимални потребан скуп метода за спремиште:

<?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);
}

Ове методе би се могле имплементирати кроз Елокуент опсеге, али преоптерећење класа ентитета одговорношћу преузимања није најбоља идеја, а премештање ове одговорности на класе спремишта изгледа логично. Да ли је тако? Посебно сам визуелно поделио овај интерфејс на два дела. Први део метода ће се користити у операцијама писања.

Стандардна операција писања је:

  • изградња новог објекта и изазов ПостРепоситори::саве
  • ПостРепоситори::гетБиИд, манипулација ентитетима и позивање ПостРепоситори::саве
  • изазов ПостРепоситори::делете

Операције писања не користе методе преузимања. У операцијама читања користе се само методе гет*. Ако читате о Принцип сегрегације интерфејса (писмо I в ЧВРСТО), тада ће постати јасно да је наш интерфејс превелик и да обавља најмање две различите одговорности. Време је да се подели на два. Метод гетБиИд је неопходно у оба, али како апликација буде сложенија, њене имплементације ће се разликовати. Видећемо ово мало касније. Писао сам о бескорисности дела за писање у претходном чланку, тако да ћу у овом једноставно заборавити на то.

Чини ми се да део Реад није толико бескорисан, јер чак и за Елокуент овде може бити неколико имплементација. Како назвати класу? Моћи РеадПостРепоситори, али на шаблон складиште он већ има мало значаја. Можеш само ПостКуериес:

<?php
interface PostQueries
{
    public function getById($id): Post;
    public function getLastPosts();
    public function getTopPosts();
    public function getUserPosts($userId);
}

Имплементација помоћу Елокуент-а је прилично једноставна:

<?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();
    }
}

Интерфејс мора бити повезан са имплементацијом, на пример у АппСервицеПровидер:

<?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(),
        ]);
    }
} 

метод ПостсЦонтроллер::ластПостс само тражим неку имплементацију ПостсКуериес и ради са њим. У провајдеру који смо повезали ПостКуериес са класом ЕлокуентПостКуериес и ова класа ће бити замењена у контролер.

Замислимо да је наша апликација постала веома популарна. Хиљаде корисника у минути отвара страницу са најновијим публикацијама. Најпопуларније публикације се такође читају веома често. Базе података слабо подносе таква оптерећења, па користе стандардно решење – кеш. Поред базе података, одређени снимак података се чува у складишту оптимизованом за одређене операције - мемцацхед или редис.

Логика кеширања обично није толико компликована, али њена имплементација у ЕлокуентПостКуериес није баш тачна (чак и зато што Начело појединачне одговорности). Много је природније користити шаблон Декоратер и примени кеширање као декорацију за главну акцију:

<?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();
            });
    }

    // другие методы практически такие же
}

Занемарите интерфејс складиште у конструктору. Из неког непознатог разлога, одлучили су да назову интерфејс за кеширање у Ларавел-у на овај начин.

Класа ЦацхедПостКуериес имплементира само кеширање. $тхис->цацхе->запамти проверава да ли је овај унос у кеш меморији, а ако није, онда позива повратни позив и уписује враћену вредност у кеш. Остаје само да се ова класа имплементира у апликацију. Потребне су нам све класе које у апликацији захтевају имплементацију интерфејса ПостКуериес почео да прима инстанцу класе ЦацхедПостКуериес. Међутим, он сам ЦацхедПостКуериес конструктор мора да прими класу као параметар ЕлокуентПостКуериеспошто не може да функционише без „праве“ имплементације. Мењамо АппСервицеПровидер:

<?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);
    }
}

Све моје жеље су сасвим природно описане у провајдеру. Тако смо имплементирали кеширање за наше захтеве само писањем једне класе и променом конфигурације контејнера. Остатак кода апликације није промењен.

Наравно, да бисте у потпуности имплементирали кеширање, потребно је и имплементирати поништавање како обрисани чланак не би висио на сајту неко време, већ се одмах брише. Али ово су мање ствари.

Закључак: користили смо не један, већ два шаблона. Узорак Одвајање одговорности за командни упит (ЦКРС) предлаже потпуно одвајање операција читања и писања на нивоу интерфејса. Дошао сам до њега преко Принцип сегрегације интерфејса, што сугерише да вешто манипулишем обрасцима и принципима и изводим један из другог као теорему :) Наравно, није сваком пројекту потребна таква апстракција за одабир ентитета, али ћу поделити трик са вама. У почетној фази примене развој, можете једноставно креирати класу ПостКуериес са уобичајеном имплементацијом преко Елокуент-а:

<?php
final class PostQueries
{
    public function getById($id): Post
    {
        return Post::findOrFail($id);
    }

    // другие методы
}

Када се појави потреба за кеширањем, једноставним потезом можете креирати интерфејс (или апстрактну класу) уместо ове класе ПостКуериес, копирајте његову имплементацију у класу ЕлокуентПостКуериес и идите на шему коју сам раније описао. Остатак кода апликације није потребно мењати.

Сви ови трикови са класама, интерфејсима, Убризгавање зависност и ЦКРС детаљно описано у моја књига „Архитектура сложених веб апликација“. Постоји и решење загонетке зашто су сви моји часови у примерима за овај чланак означени као коначни.

Извор: ввв.хабр.цом

Додај коментар