Korisna spremišta s Eloquentom?

Prošli tjedan sam napisao članak o beskorisnosti predloška Repository za entitete Eloquent, međutim, obećao mi je reći kako to djelomično iskoristiti u svoju korist. Da bih to učinio, pokušat ću analizirati kako se ovaj predložak obično koristi u projektima. Minimalni 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 je odlučeno koristiti repozitorije, često im se dodaju metode za dohvaćanje 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);
}

Ove metode mogu se implementirati kroz opsege Eloquent, ali preopterećenje klasa entiteta odgovornošću za dohvaćanje samih sebe nije najbolja ideja, a premještanje ove odgovornosti na klase repozitorija čini se logičnim. Je li tako? Ovo sučelje sam posebno vizualno podijelio na dva dijela. Prvi dio metoda koristit će se u operacijama pisanja.

Standardna operacija pisanja je:

  • izgradnja novog objekta i izazov PostRepository::spremi
  • PostRepository::getById, manipulacija i prizivanje entiteta PostRepository::spremi
  • izazov PostRepository::delete

Operacije pisanja ne koriste metode dohvaćanja. U operacijama čitanja koriste se samo get* metode. Ako ste čitali o Načelo razdvajanja sučelja (pismo I в SOLID), tada će postati jasno da je naše sučelje preveliko i da obavlja najmanje dvije različite odgovornosti. Vrijeme je da se podijeli na dvoje. metoda getById je neophodan u oba, ali kako aplikacija postaje složenija, njezine implementacije će se razlikovati. To ćemo vidjeti malo kasnije. O beskorisnosti dijela za pisanje pisao sam u prošlom članku, pa ću u ovom jednostavno zaboraviti na to.

Dio Read mi se ne čini tako beskoristan, jer čak i za Eloquent ovdje može postojati nekoliko implementacija. Kako nazvati razred? Limenka ReadPostRepository, ali na predložak skladište on već ima malo važnosti. Možete samo PostQueries:

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

Implementirati ga s Eloquentom vrlo je jednostavno:

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

Sučelje mora biti povezano s implementacijom, na primjer u AppServiceProvider:

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

Ova klasa je već korisna. On svoju odgovornost shvaća oslobađanjem ili kontrolora ili klase entiteta. U kontroleru se može koristiti ovako:

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

način PostsController::lastPosts samo tražim neku implementaciju PostoviUpiti i radi s njim. U pružatelju kojeg smo povezali PostQueries s razredom EloquentPostQueries i ova klasa će biti zamijenjena u kontroler.

Zamislimo da je naša aplikacija postala vrlo popularna. Tisuće korisnika u minuti otvori stranicu s najnovijim publikacijama. Vrlo često se čitaju i najpopularnije publikacije. Baze podataka ne podnose baš najbolje takva opterećenja, pa koriste standardno rješenje - predmemoriju. Uz bazu podataka, određena snimka podataka pohranjuje se u pohranu optimiziranu za određene operacije - Memcached ili redis.

Logika predmemoriranja obično nije tako komplicirana, ali njezina implementacija u EloquentPostQueries nije baš ispravna (makar samo zato što Načelo jedinstvene odgovornosti). Mnogo je prirodnije koristiti predložak Dekorater i implementirati predmemoriranje kao ukras za glavnu akciju:

<?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 sučelje skladište u konstruktoru. Iz nepoznatog razloga odlučili su sučelje za predmemoriju u Laravelu nazvati na ovaj način.

Klasa CachedPostQueries implementira samo predmemoriranje. $this->cache->rember provjerava je li ovaj unos u predmemoriji i ako nije, poziva povratni poziv i zapisuje vraćenu vrijednost u predmemoriju. Ostaje samo implementirati ovu klasu u aplikaciju. Trebaju nam sve klase koje u aplikaciji zahtijevaju implementaciju sučelja PostQueries počeo primati instancu klase CachedPostQueries. Međutim, on sam CachedPostQueries konstruktor mora primiti klasu kao parametar EloquentPostQueriesbudući da ne može funkcionirati bez "prave" implementacije. Mijenjamo 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);
    }
}

Sve moje želje su sasvim prirodno opisane u pružatelju usluga. Stoga smo implementirali predmemoriju za naše zahtjeve samo pisanjem jedne klase i promjenom konfiguracije spremnika. Ostatak koda aplikacije nije promijenjen.

Naravno, za potpunu implementaciju predmemoriranja, također je potrebno implementirati validaciju tako da obrisani članak ne visi na stranici neko vrijeme, već se odmah briše. Ali to su manje stvari.

Zaključak: nismo koristili jedan, već dva predloška. Uzorak Segregacija odgovornosti za upite naredbi (CQRS) predlaže potpuno odvajanje operacija čitanja i pisanja na razini sučelja. Došao sam do njega putem Načelo razdvajanja sučelja, što sugerira da vješto manipuliram obrascima i principima i izvodim jedno iz drugog kao teorem :) Naravno, ne treba svaki projekt takve apstrakcije za odabir entiteta, ali podijelit ću trik s vama. U početnoj fazi primjene razvoj, možete jednostavno stvoriti klasu PostQueries s uobičajenom implementacijom putem Eloquenta:

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

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

Kada se pojavi potreba za predmemoriranjem, jednostavnim potezom možete stvoriti sučelje (ili apstraktnu klasu) umjesto ove klase PostQueries, kopirajte njegovu implementaciju u razred EloquentPostQueries i idite na shemu koju sam ranije opisao. Ostatak koda aplikacije ne treba mijenjati.

Svi ti trikovi s klasama, sučeljima, Injekcija ovisnosti и CQRS detaljno opisano u moja knjiga “Arhitektura složenih web aplikacija”. Postoji i rješenje zagonetke zašto su svi moji razredi u primjerima za ovaj članak označeni kao završni.

Izvor: www.habr.com

Dodajte komentar