Užitočné úložiská s Eloquent?

Minulý týždeň som písal článok o zbytočnosti šablóny Repository pre Eloquent entity, sľúbil mi však, že mi povie, ako to čiastočne využiť vo svoj prospech. Za týmto účelom sa pokúsim analyzovať, ako sa táto šablóna zvyčajne používa v projektoch. Minimálny požadovaný súbor metód pre úložisko:

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

Ak sa však v reálnych projektoch rozhodlo použiť úložiská, často sa k nim pridávajú metódy na získavanie záznamov:

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

Tieto metódy by sa dali implementovať prostredníctvom rozsahov Eloquent, ale preťaženie tried entít zodpovednosťou za samotné načítanie nie je najlepší nápad a presun tejto zodpovednosti na triedy úložiska sa zdá byť logický. Je to tak? Toto rozhranie som konkrétne vizuálne rozdelil na dve časti. Prvá časť metód bude použitá pri operáciách zápisu.

Štandardná operácia zápisu je:

  • výstavba nového objektu a výzva PostRepository::uložiť
  • PostRepository::getById, manipulácia a privolávanie entít PostRepository::uložiť
  • výzvu PostRepository::delete

Operácie zápisu nepoužívajú metódy načítania. V operáciách čítania sa používajú iba metódy get*. Ak čítate o Princíp segregácie rozhrania (list I в SOLID), potom bude jasné, že naše rozhranie je príliš veľké a plní aspoň dve rôzne zodpovednosti. Je čas rozdeliť to dvoma. Metóda getById je potrebné v oboch, ale ako sa aplikácia stáva zložitejšou, jej implementácie sa budú líšiť. To uvidíme o niečo neskôr. O zbytočnosti písomnej časti som písal v predchádzajúcom článku, takže v tomto na to jednoducho zabudnem.

Časť Read sa mi nezdá až taká zbytočná, keďže aj pre Eloquent tu môže byť niekoľko implementácií. Ako pomenovať triedu? Môcť ReadPostRepository, ale k šablóne sklad už má malý význam. Môžete len PostQueries:

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

Jeho implementácia pomocou Eloquent je pomerne jednoduchá:

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

Rozhranie musí byť spojené s implementáciou, napríklad v AppServiceProvider:

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

Táto trieda je už užitočná. Svoju zodpovednosť si uvedomuje uvoľnením buď kontrolórov, alebo triedy subjektov. V ovládači sa dá použiť takto:

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

metóda PostsController::lastPosts len žiadam o nejakú implementáciu PostsQueries a pracuje s ním. V poskytovateľovi, ktorého sme prepojili PostQueries s triedou EloquentPostQueries a táto trieda bude nahradená do ovládača.

Predstavme si, že naša aplikácia sa stala veľmi populárnou. Stránku s najnovšími publikáciami otvoria tisíce používateľov za minútu. Veľmi často sa čítajú aj najobľúbenejšie publikácie. Databázy takéto zaťaženie veľmi nezvládajú, preto využívajú štandardné riešenie – cache. Okrem databázy sa v úložisku optimalizovanom pre určité operácie ukladá aj určitá snímka údajov – memcached alebo REDIS.

Logika ukladania do vyrovnávacej pamäte zvyčajne nie je taká zložitá, ale jej implementácia v EloquentPostQueries nie je príliš správna (už len preto, Zásada jedinej zodpovednosti). Oveľa prirodzenejšie je použiť šablónu Dekoratér a implementujte ukladanie do vyrovnávacej pamäte ako dekoráciu pre hlavnú akciu:

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

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

Ignorujte rozhranie sklad v konštruktérovi. Z neznámeho dôvodu sa rozhodli takto pomenovať rozhranie pre ukladanie do vyrovnávacej pamäte v Laravel.

Trieda CachedPostQueries implementuje iba ukladanie do vyrovnávacej pamäte. $this->cache->pamätať skontroluje, či je tento záznam vo vyrovnávacej pamäti a ak nie, zavolá spätné volanie a zapíše vrátenú hodnotu do vyrovnávacej pamäte. Zostáva už len implementovať túto triedu do aplikácie. Potrebujeme všetky triedy, ktoré v aplikácii žiadajú implementáciu rozhrania PostQueries začal dostávať inštanciu triedy CachedPostQueries. Avšak on sám CachedPostQueries konštruktor musí dostať triedu ako parameter EloquentPostQuerieskeďže bez „skutočnej“ implementácie to nemôže fungovať. meníme sa 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);
    }
}

Všetky moje priania sú úplne prirodzene popísané v poskytovateľovi. Preto sme implementovali ukladanie do vyrovnávacej pamäte pre naše požiadavky iba napísaním jednej triedy a zmenou konfigurácie kontajnera. Zvyšok kódu aplikácie sa nezmenil.

Pre plnohodnotnú implementáciu cachovania je samozrejme potrebné implementovať aj zneplatnenie, aby vymazaný článok nejaký čas nevisel na stránke, ale bol okamžite zmazaný. Ale to sú drobnosti.

Zrátané a podčiarknuté: použili sme nie jednu, ale dve šablóny. Ukážka Segregácia zodpovednosti za príkazový dotaz (CQRS) navrhuje úplne oddeliť operácie čítania a zápisu na úrovni rozhrania. Prišiel som k nemu cez Princíp segregácie rozhrania, čo naznačuje, že šikovne manipulujem so vzormi a princípmi a odvodzujem jeden od druhého ako vetu :) Samozrejme, nie každý projekt potrebuje takúto abstrakciu na výber entít, ale podelím sa s vami o trik.V počiatočnej fáze aplikácie vývoj, môžete jednoducho vytvoriť triedu PostQueries s obvyklou implementáciou cez Eloquent:

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

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

Keď vznikne potreba ukladania do vyrovnávacej pamäte, jednoduchým pohybom môžete namiesto tejto triedy vytvoriť rozhranie (alebo abstraktnú triedu) PostQueries, skopírujte jeho implementáciu do triedy EloquentPostQueries a prejdite na schému, ktorú som opísal vyššie. Zvyšok kódu aplikácie nie je potrebné meniť.

Všetky tieto triky s triedami, rozhraniami, Injekcia závislostí и CQRS podrobne popísané v moja kniha „Architektúra komplexných webových aplikácií“. Existuje aj riešenie hádanky, prečo sú všetky moje triedy v príkladoch k tomuto článku označené ako konečné.

Zdroj: hab.com

Pridať komentár