مخازن مفید با Eloquent؟

هفته پیش نوشتم مقاله ای در مورد بی فایده بودن قالب Repository برای موجودیت های Eloquentبا این حال، او به من قول داد که به من بگوید چگونه تا حدی از آن به نفع خود استفاده کنم. برای انجام این کار، سعی می کنم نحوه استفاده از این الگو را در پروژه ها تحلیل کنم. حداقل مجموعه روش های مورد نیاز برای یک مخزن:

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

با این حال، در پروژه های واقعی، اگر تصمیم به استفاده از مخازن گرفته شد، اغلب روش هایی برای بازیابی رکوردها به آنها اضافه می شود:

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

این روش ها را می توان از طریق دامنه های Eloquent پیاده سازی کرد، اما بارگذاری بیش از حد کلاس های موجود با مسئولیت واکشی خود بهترین ایده نیست و انتقال این مسئولیت به کلاس های مخزن منطقی به نظر می رسد. آیا اینطور است؟ من به طور خاص این رابط را به دو بخش تقسیم کردم. بخش اول روش ها در عملیات نوشتن استفاده خواهد شد.

عملیات نوشتن استاندارد عبارت است از:

  • ساخت یک شی جدید و چالش PostRepository::save
  • PostRepository::getById، دستکاری و احضار موجودیت PostRepository::save
  • چالش PostRepository::حذف

عملیات نوشتن از روش های واکشی استفاده نمی کند. در عملیات خواندن فقط از روش های get* استفاده می شود. اگر در مورد آن بخوانید اصل جداسازی رابط (حرف I в جامد، سپس مشخص می شود که رابط ما بسیار بزرگ است و حداقل دو مسئولیت متفاوت را انجام می دهد. وقت آن است که آن را بر دو تقسیم کنیم. روش getById در هر دو مورد ضروری است، اما با پیچیده تر شدن برنامه، پیاده سازی آن متفاوت خواهد بود. این را کمی بعد خواهیم دید. من در مورد بی فایده بودن بخش نوشتن در مقاله قبلی نوشتم، بنابراین در این مقاله به سادگی آن را فراموش خواهم کرد.

بخش Read به نظر من چندان بی فایده نیست، زیرا حتی برای Eloquent ممکن است چندین پیاده سازی در اینجا وجود داشته باشد. نام کلاس را چه کنیم؟ می توان ReadPostRepository، اما به الگو مخزن او قبلاً ارتباط کمی دارد. شما فقط می توانید PostQueries:

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

پیاده سازی آن با Eloquent بسیار ساده است:

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

رابط باید با پیاده سازی مرتبط باشد، به عنوان مثال در AppServiceProvider:

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

این کلاس قبلاً مفید است. او با رهایی از کنترلرها یا کلاس موجودیت متوجه مسئولیت خود می شود. در یک کنترلر می توان از آن به صورت زیر استفاده کرد:

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

روش PostsController::lastPosts فقط درخواست اجرا کردن PostQuery و با آن کار می کند. در ارائه دهنده ای که پیوند داده ایم PostQueries با کلاس EloquentPostQueries و این کلاس در کنترلر جایگزین می شود.

بیایید تصور کنیم که برنامه ما بسیار محبوب شده است. هزاران کاربر در دقیقه صفحه را با آخرین انتشارات باز می کنند. محبوب ترین نشریات نیز اغلب خوانده می شوند. پایگاه های داده به خوبی چنین بارهایی را مدیریت نمی کنند، بنابراین از یک راه حل استاندارد - یک کش استفاده می کنند. علاوه بر پایگاه داده، یک عکس فوری داده خاص در فضای ذخیره سازی بهینه شده برای عملیات خاص ذخیره می شود - memcached یا قرمز شدن.

منطق کش معمولاً چندان پیچیده نیست، اما پیاده سازی آن در EloquentPostQueries چندان صحیح نیست (البته فقط به این دلیل که اصل مسئولیت منفرد). استفاده از قالب بسیار طبیعی تر است تزئین کننده و ذخیره کش را به عنوان تزئین برای عمل اصلی پیاده سازی کنید:

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

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

رابط را نادیده بگیرید مخزن در سازنده به دلایلی نامعلوم، آنها تصمیم گرفتند این رابط را برای کش در لاراول نامگذاری کنند.

کلاس CachedPostQueries فقط کش را پیاده سازی می کند. $this->cache->remember بررسی می کند که آیا این ورودی در حافظه نهان وجود دارد یا خیر و اگر نه، سپس callback می کند و مقدار برگشتی را در حافظه پنهان می نویسد. تنها چیزی که باقی می ماند پیاده سازی این کلاس در برنامه است. ما به تمام کلاس هایی نیاز داریم که در برنامه درخواست پیاده سازی اینترفیس را داشته باشند PostQueries شروع به دریافت نمونه ای از کلاس کرد CachedPostQueries. با این حال، خود او CachedPostQueries سازنده باید یک کلاس را به عنوان پارامتر دریافت کند EloquentPostQueriesزیرا بدون اجرای "واقعی" نمی تواند کار کند. ما تغییر می کنیم 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);
    }
}

تمام خواسته های من کاملاً طبیعی در ارائه دهنده توضیح داده شده است. بنابراین، ما کش را برای درخواست های خود تنها با نوشتن یک کلاس و تغییر پیکربندی کانتینر پیاده سازی کردیم. بقیه کد برنامه تغییر نکرده است.

البته برای پیاده سازی کامل کش نیز لازم است که Invalidation نیز پیاده سازی شود تا مقاله حذف شده مدتی در سایت آویزان نشود، بلکه بلافاصله حذف شود. اما اینها چیزهای جزئی هستند.

خط پایین: ما نه یک، بلکه از دو الگو استفاده کردیم. نمونه تفکیک مسئولیت پرس و جو فرمان (CQRS) پیشنهاد می کند تا عملیات خواندن و نوشتن را در سطح رابط کاملاً جدا کنید. من از طریق او آمدم اصل جداسازی رابط، که نشان می دهد من به طرز ماهرانه ای الگوها و اصول را دستکاری می کنم و یکی را به عنوان قضیه از دیگری استخراج می کنم :) البته هر پروژه ای برای انتخاب موجودیت ها به چنین انتزاعی نیاز ندارد اما این ترفند را با شما در میان می گذارم.در مرحله اولیه کاربرد توسعه، شما به سادگی می توانید یک کلاس ایجاد کنید PostQueries با اجرای معمول از طریق Eloquent:

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

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

هنگامی که نیاز به ذخیره سازی وجود دارد، با یک حرکت ساده می توانید یک رابط (یا کلاس انتزاعی) به جای این کلاس ایجاد کنید. PostQueries، پیاده سازی آن را در کلاس کپی کنید EloquentPostQueries و به طرحی که قبلا توضیح دادم بروید. بقیه کد برنامه نیازی به تغییر ندارد.

همه این ترفندها با کلاس ها، رابط ها، تزریق وابستگی и CQRS به تفصیل در کتاب من "معماری برنامه های پیچیده وب". همچنین راه حلی برای معما وجود دارد که چرا تمام کلاس های من در مثال های این مقاله به عنوان نهایی علامت گذاری شده اند.

منبع: www.habr.com

اضافه کردن نظر