Korisni repozitoriji za Eloquent?

Prošli tjedan sam napisao članak o beskorisnosti Repository predloška za Eloquent entitete, no obećao sam objasniti kako se može djelomično iskoristiti s koristima. Pokušat ću analizirati kako se obično koristi ovaj predložak u projektima. Minimalno potrebni skup metoda za repozitorij:

<?php
interface PostRepository
{
    public function getById($id): Post;
    public function save(Post $post);
    public function delete($id);
}

Međutim, u stvarnim projektima, ako se odluči koristiti repozitorije, često im se dodaju metode za preuzimanje zapisa:

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

Te se metode mogu implementirati putem Eloquent opsega, ali preopteretiti klase entiteta odgovornostima preuzimanja samih sebe — nije baš najbolja zamisao i premještanje te odgovornosti u klase repozitorija djeluje logično. Je li to tako? Posebno sam vizualno podijelio ovaj sučelje na dva dijela. Prvi dio metoda će biti korišten u operacijama zapisivanja.

Standardne operacije zapisivanja su:

  • konstruiranje novog objekta i poziv PostRepository::save
  • PostRepository::getById, manipulacije s entitetom i poziv PostRepository::save
  • poziv PostRepository::delete

U operacijama zapisivanja ne koriste se metode preuzimanja. U operacijama čitanja koriste se samo metode get*. Ako pročitate o Principu razdvajanja sučelja (slovo I u SOLID), bit će jasno da je naše sučelje postalo preveliko i ispunjava najmanje dvije različite odgovornosti. Vrijeme je da ga podijelimo na dva. Metoda getById neophodna je u oba, no s komplikacijama aplikacije njene implementacije će biti različite. To ćemo vidjeti malo kasnije. O beskorisnosti write dijela sam pisao u prošlom članku, stoga ću ga ovdje jednostavno zaboraviti.

Read dijelovi mi se ne čine tako beskorisni, jer čak i za Eloquent ovdje može biti nekoliko implementacija. Kako nazvati klasu? Može ReadPostRepository, ali s predloškom Repository ima već malo veze. Može jednostavno PostQueries:

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

Njegova implementacija putem Eloquent-a je prilično jednostavna:

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

Interfejs mora biti povezan s implementacijom, na primjer u AppServiceProvider:

app->bind(PostQueries::class, 
            EloquentPostQueries::class);
    }
}

Ova klasa je već korisna. Ona izvršava svoju odgovornost, rasterećujući time ili kontrolere ili klasu entiteta. U kontroleru može biti korištena ovako:

$postQueries->getLastPosts(),
        ]);
    }
} 

Metoda PostsController::lastPosts jednostavno traži neku implementaciju za sebe PostsQueries i radi s njom. U provajderu smo povezali PostQueries s klasom EloquentPostQueries i u kontroler će biti umetnuta ta klasa.

Zamislimo da je naša aplikacija postala vrlo popularna. Tisuće korisnika u minuti otvaraju stranicu s najnovijim objavama. Najpopularnije objave također se često čitaju. Baze podataka ne podnose dobro takva opterećenja, pa se koristi standardno rješenje - cache. Pored baze podataka, neki presjek podataka pohranjuje se u skladištu optimiziranom za određene operacije - memcached ili redis.

Logika keširanja obično nije toliko složena, ali implementirati je u EloquentPostQueries nije baš ispravno (barem zbog Single Responsibility Principle). Puno prirodnije je koristiti obrazac Dekorator i implementirati keširanje kao dekoriranje glavne funkcije:

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

    // ostale metode su praktički iste
}

Ne obraćajte pažnju na sučelje Repository u konstruktoru. Iz nepoznatog razloga, tako su odlučili nazvati sučelje za keširanje u Laravelu.

Klasa CachedPostQueries realizira samo keširanje. $this->cache->remember provjerava postoji li ta stavka u kešu i ako ne postoji, poziva callback i zapisuje u keš vrijednost koja se vratila. Ostalo je samo implementirati ovu klasu u aplikaciju. Potrebno je da sve klase koje u aplikaciji traže implementaciju sučelja PostQueries počnu primati instancu klase CachedPostQueries. Međutim, sam CachedPostQueries kao parametar u konstruktor mora dobiti klasu EloquentPostQueries, budući da ne može raditi bez "prave" implementacije. Mijenjamo AppServiceProvider:

app->bind(PostQueries::class, 
            CachedPostQueries::class);

        $this->app->when(CachedPostQueries::class)
            ->needs(PostQueries::class)
            ->give(EloquentPostQueries::class);
    }
}

Sve moje želje prilično su prirodno opisane u provajderu. Tako smo implementirali keširanje za naše upite samo pisanjem jedne klase i promjenom konfiguracije kontejnera. Kod ostatka aplikacije nije se promijenio.

Naravno, za potpunu implementaciju keširanja potrebno je još implementirati invalidaciju, kako bi uklonjena stavka odmah nestala sa stranice, a ne ostala neko vrijeme. Ali to su već sitnice.

Zaključak: koristili smo ne jedan, već dva obrasca. Obrazac Command Query Responsibility Segregation (CQRS) predlaže potpuno odvajanje operacija čitanja i pisanja na razini sučelja. Došao sam do njega kroz Principu razdvajanja sučelja, što pokazuje da vješto manipuliram obrascima i principima i izvodim jedan iz drugog kao teoremu 🙂 Naravno, ne mora svakom projektu biti potrebna takva apstrakcija za pretragu entiteta, ali ću podijeliti s vama trik. U početnoj fazi razvoja aplikacije, jednostavno možete stvoriti klasu PostQueries s uobičajenom implementacijom kroz Eloquent:

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

    // druge metode
}

Kada se pojavi potreba za keširanjem, jednostavnim pokretom možete stvoriti sučelje (ili apstraktnu klasu) umjesto ove klase PostQueries, njegovu implementaciju kopirati u klasu EloquentPostQueries i preći na shemu koju sam opisao ranije. Ostali kod aplikacije ne treba mijenjati.

Sve te trikove s klasama, sučeljima, Dependency Injection i CQRS detaljno opisujem u mojoj knjizi "Arhitektura složenih web aplikacija". Tamo je i rješenje zagonetke zašto su sve moje klase u primjerima ovog članka označene kao final.

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster