Mga kapaki-pakinabang na repository na may Eloquent?

Noong nakaraang linggo nagsulat ako artikulo tungkol sa kawalan ng silbi ng template ng Repository para sa Eloquent entity, gayunpaman, nangako siyang sasabihin sa akin kung paano ito bahagyang gagamitin sa kanyang kalamangan. Upang gawin ito, susubukan kong suriin kung paano karaniwang ginagamit ang template na ito sa mga proyekto. Ang minimum na kinakailangang hanay ng mga pamamaraan para sa isang repositoryo:

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

Gayunpaman, sa mga totoong proyekto, kung napagpasyahan na gumamit ng mga repositoryo, ang mga pamamaraan para sa pagkuha ng mga tala ay madalas na idinagdag sa kanila:

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

Ang mga pamamaraang ito ay maaaring ipatupad sa pamamagitan ng Eloquent scope, ngunit ang labis na karga ng mga klase ng entity na may responsibilidad na kunin ang kanilang mga sarili ay hindi ang pinakamahusay na ideya, at ang paglipat ng responsibilidad na ito sa mga klase ng repositoryo ay tila lohikal. Ganoon ba? Partikular kong hinati ang interface na ito sa dalawang bahagi. Ang unang bahagi ng mga pamamaraan ay gagamitin sa pagsulat ng mga operasyon.

Ang karaniwang operasyon ng pagsulat ay:

  • pagbuo ng isang bagong bagay at hamon PostRepository::save
  • PostRepository::getById, pagmamanipula at pagpapatawag ng entity PostRepository::save
  • isang hamon PostRepository::tanggalin

Ang mga operasyon sa pagsulat ay hindi gumagamit ng mga paraan ng pagkuha. Sa mga read operation, get* method lang ang ginagamit. Kung babasahin mo ang tungkol sa Prinsipyo ng Interface Segregation (sulat I Π² Matatag), pagkatapos ay magiging malinaw na ang aming interface ay masyadong malaki at gumaganap ng hindi bababa sa dalawang magkaibang mga responsibilidad. Panahon na upang hatiin ito sa dalawa. Pamamaraan getById ay kinakailangan sa pareho, ngunit habang ang aplikasyon ay nagiging mas kumplikado, ang mga pagpapatupad nito ay mag-iiba. Makikita natin ito mamaya. Isinulat ko ang tungkol sa kawalan ng silbi ng bahagi ng pagsulat sa isang nakaraang artikulo, kaya sa isang ito ay kakalimutan ko na lang ito.

Ang bahagi ng Basahin ay tila sa akin ay hindi gaanong walang silbi, dahil kahit na para sa Eloquent ay maaaring mayroong ilang mga pagpapatupad dito. Ano ang ipapangalan sa klase? Pwede ReadPostRepository, ngunit sa template Repository maliit na ang kaugnayan niya. pwede lang Mga PostQuery:

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

Ang pagpapatupad nito sa Eloquent ay medyo simple:

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

Ang interface ay dapat na nauugnay sa pagpapatupad, halimbawa sa AppServiceProvider:

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

Kapaki-pakinabang na ang klase na ito. Napagtanto niya ang kanyang responsibilidad sa pamamagitan ng pag-alis sa alinman sa mga controllers o sa klase ng entity. Sa isang controller maaari itong gamitin tulad nito:

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

pamamaraan PostsController::lastPosts humihingi lang ng implementasyon Mga PostQuery at gumagana kasama nito. Sa provider na na-link namin Mga PostQuery may klase EloquentPostQueries at ang klase na ito ay papalitan sa controller.

Isipin natin na ang aming aplikasyon ay naging napakapopular. Libu-libong user kada minuto ang nagbubukas ng page gamit ang mga pinakabagong publikasyon. Ang pinakasikat na mga publikasyon ay madalas ding binabasa. Ang mga database ay hindi pinangangasiwaan nang mahusay ang mga naturang load, kaya gumagamit sila ng isang karaniwang solusyon - isang cache. Bilang karagdagan sa database, ang isang partikular na snapshot ng data ay naka-imbak sa storage na na-optimize para sa ilang mga operasyon - memcached o redis.

Ang lohika ng pag-cache ay kadalasang hindi ganoon kakomplikado, ngunit ang pagpapatupad nito sa EloquentPostQueries ay hindi masyadong tama (kung dahil lamang Single Prinsipyo ng Responsibilidad). Mas natural na gumamit ng template Dekorador at ipatupad ang caching bilang dekorasyon para sa pangunahing aksyon:

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

    // Π΄Ρ€ΡƒΠ³ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ практичСски Ρ‚Π°ΠΊΠΈΠ΅ ΠΆΠ΅
}

Huwag pansinin ang interface Repository sa constructor. Para sa ilang hindi kilalang dahilan, nagpasya silang pangalanan ang interface para sa pag-cache sa Laravel sa ganitong paraan.

klase CachedPostQueries nagpapatupad lamang ng caching. $this->cache->tandaan sinusuri kung ang entry na ito ay nasa cache at kung wala, pagkatapos ay tatawag ng callback at isusulat ang ibinalik na halaga sa cache. Ang natitira na lang ay ipatupad ang klase na ito sa application. Kailangan namin ang lahat ng mga klase na nasa application upang humiling ng pagpapatupad ng interface Mga PostQuery nagsimulang makatanggap ng isang halimbawa ng klase CachedPostQueries. Gayunpaman, siya mismo CachedPostQueries ang constructor ay dapat makatanggap ng isang klase bilang isang parameter EloquentPostQueriesdahil hindi ito gagana nang walang "tunay" na pagpapatupad. Nagbabago kami 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);
    }
}

Ang lahat ng aking mga kahilingan ay natural na inilarawan sa provider. Kaya, ipinatupad namin ang pag-cache para sa aming mga kahilingan sa pamamagitan lamang ng pagsulat ng isang klase at pagbabago ng configuration ng container. Ang natitirang code ng aplikasyon ay hindi nagbago.

Siyempre, upang ganap na maipatupad ang pag-cache, kinakailangan ding ipatupad ang kawalan ng bisa upang ang tinanggal na artikulo ay hindi mag-hang sa site nang ilang panahon, ngunit agad na matanggal. Ngunit ito ay mga maliliit na bagay.

Bottom line: hindi kami gumamit ng isa, ngunit dalawang template. Sample Command Query Responsibility Segregation (CQRS) nagmumungkahi na ganap na paghiwalayin ang mga operasyon sa pagbasa at pagsulat sa antas ng interface. Lumapit ako sa kanya sa pamamagitan ng Prinsipyo ng Interface Segregation, na nagmumungkahi na mahusay kong manipulahin ang mga pattern at prinsipyo at nakukuha ang isa mula sa isa bilang isang theorem :) Siyempre, hindi lahat ng proyekto ay nangangailangan ng ganoong abstraction para sa pagpili ng mga entity, ngunit ibabahagi ko sa iyo ang trick. Sa paunang yugto ng aplikasyon pag-unlad, maaari kang lumikha lamang ng isang klase Mga PostQuery sa karaniwang pagpapatupad sa pamamagitan ng Eloquent:

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

    // Π΄Ρ€ΡƒΠ³ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹
}

Kapag ang pangangailangan para sa pag-cache ay lumitaw, sa isang simpleng paglipat maaari kang lumikha ng isang interface (o abstract na klase) sa lugar ng klase na ito Mga PostQuery, kopyahin ang pagpapatupad nito sa klase EloquentPostQueries at pumunta sa scheme na inilarawan ko kanina. Ang natitirang code ng aplikasyon ay hindi kailangang baguhin.

Lahat ng mga trick na ito na may mga klase, interface, Depensyon ng Iniksyon ΠΈ CQRS inilarawan nang detalyado sa ang aking aklat na "Arkitektura ng Mga Kumplikadong Web Application". Mayroon ding solusyon sa bugtong kung bakit ang lahat ng aking mga klase sa mga halimbawa para sa artikulong ito ay minarkahan bilang pangwakas.

Pinagmulan: www.habr.com

Magdagdag ng komento