Uporabni repozitoriji z Eloquentom?

Prejšnji teden sem pisal članek o neuporabnosti predloge Repository za entitete Eloquent, pa mi je obljubil, da mi bo povedal, kako naj to delno uporabi sebi v prid. Da bi to naredil, bom poskušal analizirati, kako se ta predloga običajno uporablja v projektih. Najmanjši zahtevani nabor metod za repozitorij:

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

Vendar pa se v resničnih projektih, če se je odločilo za uporabo repozitorijev, jim pogosto dodajo metode za pridobivanje zapisov:

<?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 metode bi lahko implementirali prek obsegov Eloquent, vendar preobremenitev razredov entitet z odgovornostjo za pridobivanje samih sebe ni najboljša ideja in prenos te odgovornosti na razrede repozitorija se zdi logičen. Je tako Ta vmesnik sem posebej vizualno razdelil na dva dela. Prvi del metod bo uporabljen pri operacijah pisanja.

Standardna operacija pisanja je:

  • gradnja novega objekta in izziv PostRepository::save
  • PostRepository::getById, manipulacija in priklic entitet PostRepository::save
  • izziv PostRepository::delete

Operacije pisanja ne uporabljajo metod pridobivanja. V operacijah branja se uporabljajo samo metode get*. Če berete o Načelo ločevanja vmesnikov (pismo I в SOLID), potem bo postalo jasno, da je naš vmesnik prevelik in opravlja vsaj dve različni odgovornosti. Čas je, da ga razdelimo na dva. Metoda getById je potreben v obeh, vendar ko postane aplikacija bolj zapletena, se bodo njene izvedbe razlikovale. To bomo videli malo kasneje. O neuporabnosti pisanja sem pisal v prejšnjem članku, zato bom v tem nanj enostavno pozabil.

Del Read se mi ne zdi tako neuporaben, saj je tudi za Eloquent tukaj lahko več implementacij. Kako poimenovati razred? Lahko ReadPostRepository, ampak na predlogo Repozitorij že ima malo pomena. Lahko samo PostQueries:

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

Implementacija z Eloquentom je precej preprosta:

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

Vmesnik mora biti povezan z izvedbo, na primer v AppServiceProvider:

<?php
final class AppServiceProvider extends ServiceProvider 
{
    public function register()
    {
        $this->app->bind(PostQueries::class, 
            EloquentPostQueries::class);
    }
}

Ta razred je že uporaben. Svojo odgovornost uresničuje z razbremenitvijo bodisi kontrolorjev bodisi entitetnega razreda. V krmilniku se lahko uporablja takole:

<?php
final class PostsController extends Controller
{
    public function lastPosts(PostQueries $postQueries)
    {
        return view('posts.last', [
            'posts' => $postQueries->getLastPosts(),
        ]);
    }
} 

Metoda PostsController::lastPosts samo prosim za nekaj izvedbe PostsQueries in dela z njim. Pri ponudniku, ki smo ga povezali PostQueries z razredom EloquentPostQueries in ta razred bo zamenjan v krmilniku.

Predstavljajmo si, da je naša aplikacija postala zelo priljubljena. Na tisoče uporabnikov na minuto odpre stran z najnovejšimi publikacijami. Zelo pogosto se berejo tudi najbolj priljubljene publikacije. Podatkovne baze tovrstnih obremenitev slabo prenašajo, zato uporabljajo standardno rešitev – predpomnilnik. Poleg baze podatkov je določen posnetek podatkov shranjen v shrambi, ki je optimizirana za določene operacije - memcached ali redis.

Logika predpomnjenja običajno ni tako zapletena, vendar njena implementacija v EloquentPostQueries ni zelo pravilna (čeprav zato, ker Načelo enotne odgovornosti). Veliko bolj naravno je uporabiti šablono Dekorater in implementirajte predpomnjenje kot okras za glavno dejanje:

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

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

Ignorirajte vmesnik Repozitorij v konstruktorju. Iz neznanega razloga so se tako odločili poimenovati vmesnik za predpomnjenje v Laravelu.

Razred CachedPostQueries izvaja samo predpomnjenje. $this->cache->remember preveri, ali je ta vnos v predpomnilniku, in če ni, pokliče povratni klic in zapiše vrnjeno vrednost v predpomnilnik. Vse kar ostane je implementacija tega razreda v aplikacijo. Potrebujemo vse razrede v aplikaciji, da zahtevamo izvedbo vmesnika PostQueries začel prejemati primerek razreda CachedPostQueries. Vendar pa on sam CachedPostQueries konstruktor mora prejeti razred kot parameter EloquentPostQueriessaj ne more delovati brez "prave" implementacije. Spremenimo se 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);
    }
}

Vse moje želje so povsem naravno opisane pri ponudniku. Tako smo izvedli predpomnjenje za naše zahteve le tako, da smo napisali en razred in spremenili konfiguracijo vsebnika. Preostala koda aplikacije se ni spremenila.

Seveda je za popolno implementacijo predpomnjenja potrebno implementirati tudi razveljavljanje, tako da izbrisani članek nekaj časa ne visi na spletnem mestu, ampak se takoj izbriše. Ampak to so manjše stvari.

Bistvo: uporabili nismo eno, ampak dve predlogi. Vzorec Ločevanje odgovornosti za ukazno poizvedbo (CQRS) predlaga popolno ločitev operacij branja in pisanja na ravni vmesnika. Skozi sem prišel do njega Načelo ločevanja vmesnikov, kar nakazuje, da spretno manipuliram z vzorci in principi ter izpeljem eno iz drugega kot izrek :) Seveda ne potrebuje vsak projekt take abstrakcije za izbiro entitet, vendar bom z vami delil trik. V začetni fazi aplikacije razvoj, lahko preprosto ustvarite razred PostQueries z običajno izvedbo prek Eloquenta:

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

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

Ko se pojavi potreba po predpomnjenju, lahko s preprosto potezo ustvarite vmesnik (ali abstraktni razred) namesto tega razreda PostQueries, kopirajte njegovo izvedbo v razred EloquentPostQueries in pojdite na shemo, ki sem jo opisal prej. Ostale kode aplikacije ni treba spreminjati.

Vsi ti triki z razredi, vmesniki, Vbrizgavanje odvisnosti и CQRS podrobno opisano v moja knjiga "Arhitektura kompleksnih spletnih aplikacij". Obstaja tudi rešitev uganke, zakaj so vsi moji razredi v primerih za ta članek označeni kot končni.

Vir: www.habr.com

Dodaj komentar