Korisna spremišta sa Eloquentom?

Prošle nedelje sam pisao članak o beskorisnosti predloška Repozitorija za Eloquent entitete, međutim, obećao mi je reći kako da to djelimično iskoristim 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 spremište:

<?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 da se koriste spremišta, č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 bi se mogle implementirati kroz Eloquent opsege, ali preopterećenje klasa entiteta odgovornošću da sami sebe dohvate nije najbolja ideja, a premještanje ove odgovornosti na klase spremišta izgleda logično. je li tako? Posebno sam vizuelno podelio ovaj interfejs na dva dela. Prvi dio metoda će se koristiti u operacijama pisanja.

Standardna operacija pisanja je:

  • izgradnja novog objekta i izazov PostRepository::save
  • PostRepository::getById, manipulacija entitetima i pozivanje PostRepository::save
  • izazov PostRepository::delete

Operacije pisanja ne koriste metode preuzimanja. U operacijama čitanja koriste se samo metode get*. Ako čitate o Princip segregacije interfejsa (pismo I в Čvrsti), tada će postati jasno da je naše sučelje preveliko i da obavlja najmanje dvije različite odgovornosti. Vrijeme je da ga podijelite sa dva. Metoda getById je neophodan u oba, ali kako aplikacija postaje složenija, njene implementacije će se razlikovati. To ćemo vidjeti malo kasnije. O beskorisnosti pisanja sam pisao u prethodnom članku, pa ću u ovom jednostavno zaboraviti na to.

Čini mi se da dio Read nije toliko beskorisan, jer čak i za Eloquent ovdje može postojati nekoliko implementacija. Kako nazvati klasu? Može ReadPostRepository, ali na šablon Repozitorij on je već malo relevantan. Možeš samo PostQueries:

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

Implementacija sa Eloquentom je prilično jednostavna:

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

Interfejs mora biti povezan 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. Svoju odgovornost shvata tako što razrješava ili kontrolore ili klasu 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(),
        ]);
    }
} 

Metoda PostsController::lastPosts samo tražim neku implementaciju PostsQueries i radi s njim. U provajderu kojeg smo povezali PostQueries sa klasom EloquentPostQueries i ova klasa će biti zamijenjena u kontroler.

Zamislimo da je naša aplikacija postala veoma popularna. Hiljade korisnika u minuti otvara stranicu s najnovijim publikacijama. Najpopularnije publikacije se također vrlo često čitaju. Baze podataka ne podnose takva opterećenja baš najbolje, pa koriste standardno rješenje - keš memoriju. Pored baze podataka, određeni snimak podataka se pohranjuje u skladištu optimiziranom za određene operacije - memcached ili reci ponovo.

Logika keširanja obično nije toliko komplikovana, ali implementacija u EloquentPostQueries nije baš ispravna (makar samo zato što Načelo pojedinačne odgovornosti). Mnogo je prirodnije koristiti šablon Dekorator i implementirati keširanje kao ukras za glavnu radnju:

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

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

Zanemarite interfejs Repozitorij u konstruktoru. Iz nekog nepoznatog razloga, odlučili su da nazovu interfejs za keširanje u Laravelu na ovaj način.

Класс CachedPostQueries implementira samo keširanje. $this->cache->zapamti provjerava da li je ovaj unos u keš memoriji i ako nije, onda poziva povratni poziv i upisuje vraćenu vrijednost u keš memoriju. Sve što ostaje je implementirati ovu klasu u aplikaciju. Potrebne su nam sve klase u aplikaciji da zahtevaju implementaciju interfejsa PostQueries počeo primati instancu klase CachedPostQueries. Međutim, on sam CachedPostQueries konstruktor mora primiti klasu kao parametar EloquentPostQueriespošto ne može raditi bez "prave" implementacije. Mi se mijenjamo 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 provajderu. Stoga smo implementirali keširanje za naše zahtjeve samo pisanjem jedne klase i promjenom konfiguracije kontejnera. Ostatak koda aplikacije nije promijenjen.

Naravno, da biste u potpunosti implementirali keširanje, potrebno je i implementirati poništavanje kako obrisani članak ne bi visio na stranici neko vrijeme, već se odmah izbrisao. Ali to su manje stvari.

Zaključak: koristili smo ne jedan, već dva šablona. Uzorak Odvajanje odgovornosti za naredbe (CQRS) predlaže potpuno odvajanje operacija čitanja i pisanja na nivou interfejsa. Došao sam do njega preko Princip segregacije interfejsa, što sugerira da vješto manipuliram obrascima i principima i izvodim jedan iz drugog kao teoremu :) Naravno, nije svakom projektu potrebna takva apstrakcija za odabir entiteta, ali podijelit ću trik s vama. U početnoj fazi primjene razvoj, možete jednostavno kreirati klasu PostQueries uz uobičajenu implementaciju preko Eloquenta:

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

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

Kada se ukaže potreba za keširanjem, jednostavnim potezom možete kreirati interfejs (ili apstraktnu klasu) umesto ove klase PostQueries, kopirajte njegovu implementaciju u klasu EloquentPostQueries i prijeđite na shemu koju sam ranije opisao. Ostatak koda aplikacije nije potrebno mijenjati.

Svi ovi trikovi sa klasama, interfejsima, Injekcija zavisnosti и CQRS detaljno opisano u moja knjiga “Arhitektura složenih web aplikacija”. Postoji i rješenje zagonetke zašto su svi moji časovi u primjerima za ovaj članak označeni kao konačni.

izvor: www.habr.com

Dodajte komentar