پیاده سازی فرآیند خرید محصول در لاراول با استفاده از الگوی Chain of Responsibility

پیاده سازی فرآیند خرید محصول در لاراول با استفاده از الگوی Chain of Responsibility

در بخش آموزش الگوی طراحی Chain of Responsibility از دوره الگو های طراحی با این الگو آشنا شدیم. دراین مقاله قصد داریم در سورس کد فریم ورک Laravel نمونه هایی از این الگو را مشاهده کنیم و یک پروژه کوچک را به عنوان تمرین با آن پیاده سازی کنیم.
قبل از اینکه به سراغ بخش بعدی بروید نیاز است تا یک نسخه از فریم ورک Laravel را نصب کنید. برای انجام این کار می توانید از بخش https://laravel.com/docs/8.x/installation مستندات این فریم ورک کمک بگیرید.

بررسی فرآیند bootstrap شدن فریم ورک

Bootstrap شدن شامل فرآیندی است که لاراول طی می کند تا بخش های مورد نیاز فریم ورک را آماده سازی و در کنار هم قرار دهد تا توانایی پردازش یک درخواست را داشته باشد.
پس از نصب فریم ورک پوشه vendor را باز کنید و در مسیر زیر فایل Kernel.php را باز کنید.

Laravel/framework/src/ Illuminate/ Foundation/ Http

به property با نام $bootstrappers توجه کنید.

protected $bootstrappers = [
    \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
    \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
    \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
    \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
    \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
    \Illuminate\Foundation\Bootstrap\BootProviders::class,
];

حالا به داخل متد sendRequestThroughRouter در متد handle این فایل بروید.
(می توانید با نگهداشتن Cntrl و کلیک کردن روی آنها در PhpStorm این کار را انجام دهید.)

public function handle($request)
{
    try {
        $request->enableHttpMethodParameterOverride();

        $response = $this->sendRequestThroughRouter($request);
    } catch (Exception $e) {
        $this->reportException($e);

        $response = $this->renderException($request, $e);
    } catch (Throwable $e) {
        $this->reportException($e = new FatalThrowableError($e));

        $response = $this->renderException($request, $e);
    }

    $this->app['events']->dispatch(
        new RequestHandled($request, $response)
    );

    return $response;
}

حال به داخل متد bootstrap بروید.

protected function sendRequestThroughRouter($request)
{
    $this->app->instance('request', $request);

    Facade::clearResolvedInstance('request');

    $this->bootstrap();

    return (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());
}

داخل متد bootstrapWith (در صورتی که شما به interface این متد هدایت شدید با استفاده از ابزار های ide خود به بدنه این متد بروید).

public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
        $this->app->bootstrapWith($this->bootstrappers());
    }
}

ودر نهایت به جایی که می خواستیم رسیدیم.

public function bootstrapWith(array $bootstrappers)
{
    $this->hasBeenBootstrapped = true;

    foreach ($bootstrappers as $bootstrapper) {
        $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]);

        $this->make($bootstrapper)->bootstrap($this);

        $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]);
    }
}

همانطور که میبینید در این متد روی property که در ابتدا نشان دادیم پیمایش می شود و روی هر کدام از آن کلاس ها، متد bootstrap صدا زده میشود. دقیقا همان کاری که در دوره الگو های طراحی از آن صحبت شد. اگر نویسنده این کد از الگوی chain of responsibility استفاده نمی کرد باید تمام کد هایی که در متد Bootstrap هر کدام از آن کلاس ها قرار دارد را در متد BootstrapWith پیاده سازی می کرد که ممکن بود صد ها و یا هزاران خط کد ایجاد کند و از خوانایی و قابلیت توسعه آن بسیار کاسته می شد.


بررسی قابلیت middleware

هر middleware در لاراول یک کلاس است که متد handle را در خورد دارد و شی Request را دریافت میکند، میتواند روی آن تغییراتی ایجاد کند ( مثل تبدیل رشته خالی به NULL ) یا بر اساس شرایطی اجرای ادامه چرخه را متوقف کند ( مثل درست نبودن توکن CSRF ) و یا آن را به شی بعدی بفرستد.
این فرآیند بسیار شبیه الگوی chain of responsibility است، یک درخواست به زنجیره ای از کلاس ها یا همان پردازش کننده ها ارسال میشود که ترتیب در آنها هم مهم است و همه آنها از یک قرارداد پیروی میکنند (همه متد handle را دارند ) و هر کدام از کلاس ها هم قابلیت جلوگیری از اجرای ادامه زنجیره را دارند.
نگاهی به سورس کد لاراول می اندازیم و سپس پروژه ای را با استفاده از این الگو پیاده سازی می کنیم.
به کلاس Kernel که در مسیر app/Http قرار دارد بروید.
داخل این کلاس لیستی از middleware ها به عنوان property ذخیره شده است حال درون کلاس پدر این کلاس بروید.

protected $middleware = [
    \App\Http\Middleware\TrustProxies::class,
    \App\Http\Middleware\CheckForMaintenanceMode::class,
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];

در متد handle این کلاس به sendRequestThroughRouterبروید.

public function handle($request)
{
    try {
        $request->enableHttpMethodParameterOverride();

        $response = $this->sendRequestThroughRouter($request);
    } catch (Exception $e) {
        $this->reportException($e);

        $response = $this->renderException($request, $e);
    } catch (Throwable $e) {
        $this->reportException($e = new FatalThrowableError($e));

        $response = $this->renderException($request, $e);
    }

    $this->app['events']->dispatch(
        new RequestHandled($request, $response)
    );

    return $response;
}

به 4 خط زیر دقت کنید.

return (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());

در اینجا از ویژگی pipeline لاراول استفاده شده تا درخواست را به ترتیب از بین کلاس هایی که به عنوان middleware تعیین کرده بودیم عبور دهد و به ازای هر کدام متد handle را صدا بزند، هرکدام از آن کلاس ها این قابلیت را دارند تا اجرای برنامه را متوقف کنند.
این یعنی همان مفهوم chain of responsibility.
در لاراول هنگام dispatch شدن job ها و همچنین در پکیج رسمی Spark هم از این الگو استفاده شده که اگر کنجکاو هستید میتوانید سری به سورس کد آن بخش ها هم بزنید.

پیاده سازی فرآیند خرید محصول در لاراول با استفاده از الگوی chain of responsibility

میخواهیم یک سیستم ثبت سفارش ساده داشته باشیم که در آن کاربر محصولی را خرید میکند و برای آن در سیستم ما یک سفارش ثبت می شود.
هنگامی که کاربر درخواست خرید یک محصول را می کند سوال های زیر پرسیده میشود:
• آیا ارسال به شهر کاربر امکان پذیر است؟
اگر نیست فرآیند را متوقف کن.
• آیا محصول در انبار موجود است؟
اگر هست یکی از آن کم کن و اگر نیست فرآیند خرید را متوقف کن.
• آیا کاربر موجودی کافی دارد؟
اگر دارد مبلغ محصول را از موجودی او کم کن در غیر این صورت فرآیند خرید را متوقف کن.
اگر پاسخ همه این سوال ها مثبت بود باید یک رکورد در جدول سفارشات ثبت کنیم.
ابتدا migration جدول کاربران را درست می کنیم.

Schema::create('users', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->string('city')->default('tehran');
    $table->unsignedBigInteger('credit')->default(0);
    $table->rememberToken();
    $table->timestamps();
});

سپس جدول محصولات و سفارشات.

Schema::create('products', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->string('city');
    $table->unsignedBigInteger('count');
    $table->unsignedBigInteger('price');
    $table->timestamps();
});
Schema::create('orders', function (Blueprint $table) {
    $table->unsignedBigInteger('user_id');
    $table->unsignedBigInteger('product_id');

    $table->foreign('user_id')->references('id')->on('users')
        ->onDelete('cascade')
        ->onUpdate('cascade');

    $table->foreign('product_id')->references('id')->on('products')
        ->onDelete('cascade')
        ->
Schema::create('products', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->string('city');
    $table->unsignedBigInteger('count');
    $table->unsignedBigInteger('price');
    $table->timestamps();
});

مدل محصولات را می سازیم و رابطه آن با سفارشات را درست می کنیم.

public function orders()
{
    return $this->belongsToMany(User::class,'orders');
}

در مدل user رابطه سفارشات را می سازیم.

public function orders()
{
    return $this->belongsToMany(Product::class,'orders');
}

یک کاربر و یک محصول به پایگاه داده اضافه می کنیم.
یک مسیر برای ثبت سفارش می سازیم.

Route::get('/orders/create', 'OrderController@create')->middleware('auth');

کنترلر Order را می سازیم و در متد create آن فرم ثبت سفارش را نشان می دهیم. یک مسیر برای ثبت سفارش ایجاد می کنیم.

public function create()
{
    return view('CreateOrders', [
        'products' => Product::all(),
    ]);
}


حالا به بخش اصلی یعنی فرآیند ثبت سفارش می رسیم.

Route::post('/products/{product}/buy', 'OrderController@store')->middleware('auth');

متد store را در کنترلر Order ایجاد می کنیم.

public function store(Request $request, Product $product)
{
    //todo submit order
}

یک کلاس abstract می سازیم.
این کلاس همان کنترل کننده یا (handler) است که در بخش آموزش الگوی chain of responsibility دوره الگو های طراحی از آن صحبت شد، در این کلاس هدف این است که یک قرارداد برای تمامی کلاس هایی که قرار است روی درخواست ثبت سفارش بررسی و کارهای مختلف انجام دهند ایجاد کنیم تا مطمئن شویم همه آنها متدی به نام handle را دارند. همچنین این کلاس یک property برای نگه داری شی بعدی در زنجیره، یک متد برای مشخص کردن آن و یک متد دیگر برای فراخوانی متد handle در شی بعدی داخل زنجیره دارد.
در تکه کد زیر تمامی این بخش ها به صورت کامل نوشته شده است.

abstract class OrderAcceptance
{
    /**
     * the next object in the chain
     * @var $successor
     */
    protected $successor;

    /**
     * the common method that each child class should have
     * and perform action in it like checking the users credit
     *
     * @param Request $request
     * @param Product $product
     * @return mixed
     */
    abstract public function handle(Request $request, Product $product);

    /**
     * define whats the next class
     * @param OrderAcceptance $successor
     */
    public function succeedWith(OrderAcceptance $successor)
    {
        $this->successor = $successor;
    }

    /**
     * perform the next action in the chain
     * @param Request $request
     * @param Product $product
     */
    public function next(Request $request, Product $product)
    {
        if ($this->successor) {
            $this->successor->handle($request, $product);
        }
    }

حالا هر کدام از کارهای ابتدای مطلب را به یک کلاس که از کلاس بالا ارث بری میکند تبدیل می کنیم سپس کار مورد نظر را در متد handle آن انجام می دهیم.

class OrderCanShip extends OrderAcceptance
{
    /**
     * the common method that each child class should have
     * and perform action in it like checking the users credit
     *
     * @param Request $request
     * @param Product $product
     * @return mixed
     */
    public function handle(Request $request, Product $product)
    {
        if ($request->user()->city !== $product->city) {
            abort(422, 'product con not be shipped to you');
        }
        $this->next($request, $product);
    }
}

سایر کار ها هم مانند کد بالا پیاده سازی می کنیم. ( لینک سورس کد در انتهای مطلب قرار داده شده )
حالا باید به متد store کنترلر Order برگردیم و در آن جا یک زنجیره درست کنیم و درخواست خود را وارد آن کنیم.
این یعنی همان شی مشتری.
در کنترلر Order ابتدا از کلاس هایی که میخواهیم در زنجیره ما باشند شی جدید می سازیم. ( این کار را به کمک service container لاراول هم می شود انجام داد.
سپس باید یک زنجیره درست کنیم، یعنی به اشیائی که ساختیم یک ترتیب بدهیم. این کار را با متد succeedWith انجام می دهیم و برای هر شی، شی بعدی را مشخص می کنیم.
حالا درخواست خود را از هرجای این زنجیره که می خواهیم وارد می کنیم. (متد handle آن شی در زنجیره را صدا می زنیم و درخواست خود به همراه ورودی های لازم را به آن می دهیم)
در نهایت یک سفارش جدید ثبت می کنیم. تمام این کارها در تکه کد زیر نشان داده شده است.

public function store(Request $request, Product $product)
{
    //new up process classes
    $orderCanShip = new OrderCanShip();
    $productIsAvailable = new ProductIsAvailable();
    $userHasCredit = new UserHasCredit();

    //set each classes next object in the chain
    $orderCanShip->succeedWith($productIsAvailable);
    $productIsAvailable->succeedWith($userHasCredit);

    //begin the chain from this point
    $orderCanShip->handle($request, $product);

    //if we got here everything is ok to submit an order
    $product->orders()->attach([
        'user_id' => auth()->id(),
    ]);
}

اما حتما شما هم متوجه مشکلی شده اید!
در حال حاضر اگر کاربر اقدام به خرید محصولی کند که اعتبار کافی برای خرید آن ندارد، یکی از تعداد موجودی های محصول مورد نظر کم میشود، سپس به مرحله ی بررسی موجودی میرسیم و در آن جا فرآیند را متوقف می کنیم اما تغییراتی در پایگاه داده ما صورت گرفته که ما به آنها نیازی نداشتیم.
برای رفع این مشکل از تراکنش (transaction) های پایگاه داده استفاده می کنیم.
بنابراین دو خط کد به کنترلر خود اضافه می کنیم برای ایجاد و پایان یک تراکنش.

public function store(Request $request, Product $product)
{
    DB::beginTransaction();
    //new up process classes
    $orderCanShip = new OrderCanShip();
    $productIsAvailable = new ProductIsAvailable();
    $userHasCredit = new UserHasCredit();

    //set each classes next object in the chain
    $orderCanShip->succeedWith($productIsAvailable);
    $productIsAvailable->succeedWith($userHasCredit);

    //begin the chain from this point
    $orderCanShip->handle($request, $product);

    //if we got here everything is ok to submit an order
    DB::commit();

    $product->orders()->attach([
        'user_id' => auth()->id(),
    ]);
}

و همچنین به تمامی کلاس های processor هم یک خط کد جهت برگرداندن تغییرات پایگاه داده در صورت بروز خطا اضافه می کنیم.

public function handle(Request $request, Product $product)
{
    if ($request->user()->city !== $product->city) {
        DB::rollBack();
        abort(422, 'product con not be shipped to you');
    }
    $this->next($request, $product);
}

همانطور که می بینید اضافه کردن یک عضو به این زنجیره کار بسیار راحتی است.
به عنوان مثال اگر می خواستیم یک لایه دیگر اضافه کنیم که اگر روز خرید، روز تولد کاربر بود روی فاکتور 20 درصد تخفیف اعمال کنیم، کافی است یک کلاس بسازیم و منطق این قابلیت را آنجا پیاده سازی کنیم و سپس آن را در جای مناسب به زنجیره اضافه کنیم.

در این مطلب سعی شد آموخته های خود را از قسمت chain of responsibility دوره الگو های طراحی با انجام یک پروژه کوچک و خواندن سورس کد فریم ورک لاراول به تثبیت برسانیم. در صورتی که بخشی از این مطلب دشوار به نظر می رسد در بخش نظرات با ما در میان بگذارید تا توضیحات بیشتری برای آن ارائه دهیم.