چهارشنبه, 14 مرداد 1405

الگوی Action در لاراول

...

” اگه سرویس‌هات کم‌کم بزرگ شدن و نمی‌دونی هر عملیات رو کجا قرار بدی، وقتشه با Action Pattern آشنا بشی. توی این مقاله به‌صورت ساده و عملی یاد می‌گیری Action چه کاربردی داره، چه تفاوتی با Service داره و چطور از هر دو در کنار هم استفاده کنی. “

توی دو مقاله قبلی با هم یاد گرفتیم که چطور ساختار پروژه‌های لاراول رو تمیزتر و قابل نگهداری‌تر کنیم.

اول با Repository آشنا شدیم و دیدیم که چرا بهتره منطق مربوط به کار با دیتابیس رو از کنترلر جدا کنیم.

بعد هم سراغ Service Layer رفتیم و فهمیدیم که همه منطق پروژه از جنس کار با دیتابیس نیست و بعضی وقت‌ها یه فرآیند شامل چندین مرحله مختلفه؛ مثلاً ساخت کاربر، ارسال ایمیل، ثبت لاگ و چند کار دیگه. برای همین این منطق رو داخل Service قرار دادیم تا کنترلر فقط درخواست رو دریافت کنه و پاسخ مناسب رو برگردونه.

تا اینجا همه چیز عالیه.

اما...

هرچقدر پروژه بزرگ‌تر میشه، یه سؤال جدید به وجود میاد.

آیا همه این منطق‌ها باید داخل Service نوشته بشن؟


یه مثال واقعی

فرض کن چند ماه از شروع پروژه گذشته.

کم‌کم امکانات جدید اضافه کردی.

هر بار هم برای اینکه کنترلرها تمیز بمونن، یه متد جدید به UserService اضافه کردی.

اوایل کلاس خیلی ساده بود.

class UserService
{
    public function register(array $data)
    {
        //
    }

    public function login(array $credentials)
    {
        //
    }
}

همه چیز مرتب و قابل فهمه.


اما چند ماه بعد...

همین کلاس ممکنه شبیه این بشه:

class UserService
{
    public function register(array $data) {}
    public function login(array $credentials) {}
    public function logout() {}
    public function resetPassword(string $email) {}
    public function verifyEmail(string $token) {}
    public function resendVerificationEmail(string $email) {}
    public function updateProfile(int $userId, array $data) {}
    public function changePassword(int $userId, string $oldPassword, string $newPassword) {}
    public function uploadAvatar(int $userId, $file) {}
    public function deleteAccount(int $userId) {}
    public function getUserPosts(int $userId) {}
    public function getUserComments(int $userId) {}
}

این کلاس هنوز هم کار می‌کنه.

حتی ممکنه هیچ باگی هم نداشته باشه.

اما یه سؤال مهم پیش میاد.

آیا واقعاً همه این کارها به هم مرتبط هستن؟

بیایم چندتا از متدهای بالا رو کنار هم بذاریم:

  • ثبت‌نام کاربر

  • ورود کاربر

  • تغییر رمز عبور

  • آپلود تصویر پروفایل

  • حذف حساب کاربری

  • دریافت پست‌های کاربر

همه این‌ها فقط یک وجه اشتراک دارن:

همگی به کاربر مربوط میشن.

اما از نظر مسئولیت، هیچ ارتباط مستقیمی با هم ندارن.

مثلاً تغییر رمز عبور هیچ وابستگی‌ای به آپلود تصویر پروفایل نداره.

یا حذف حساب کاربری ربطی به دریافت لیست پست‌های کاربر نداره.

در واقع فقط چون اسم کلاس UserService بوده، هر قابلیت جدیدی که مربوط به کاربر بوده داخل همین کلاس قرار گرفته.


مشکل از Service نیست

اینجا ممکنه این سؤال پیش بیاد که:

پس یعنی Service Layer ایده خوبی نیست؟

نه.

اتفاقاً Service هنوز هم یکی از مهم‌ترین بخش‌های معماری پروژه‌های لاراوله.

مشکل از جایی شروع میشه که مرز مسئولیت‌ها رو رعایت نمی‌کنیم.

کم‌کم هر کاری که به کاربر مربوط میشه رو داخل UserService قرار میدیم.

بعد از مدتی با کلاسی روبه‌رو میشیم که:

  • متدهای زیادی داره.

  • مسئولیت‌های مختلفی رو بر عهده گرفته.

  • پیدا کردن محل مناسب برای اضافه کردن قابلیت جدید سخت شده.

  • تست کردنش زمان بیشتری می‌بره.

  • درک کردنش برای اعضای جدید تیم راحت نیست.

در چنین شرایطی، خود Service مشکل نیست؛ بلکه بیش از اندازه مسئولیت گرفته.


شاید سؤال اصلی رو اشتباه پرسیدیم

تا اینجا احتمالاً ذهنمون دنبال این سؤال بوده:

"چطور Service بهتری بنویسیم؟"

اما شاید سؤال درست این باشه:

آیا همه این عملیات‌ها اصلاً باید داخل یک Service قرار بگیرن؟

فرض کن فقط می‌خوای کار ثبت‌نام کاربر رو انجام بدی.

آیا واقعاً لازمه این منطق داخل کلاسی باشه که ده‌ها مسئولیت دیگه هم داره؟

یا بهتره برای هر عملیات مهم پروژه، یه کلاس مستقل داشته باشیم؟

اینجاست که با الگویی به اسم Action Pattern آشنا میشیم.


Action Pattern چیست؟

اگه بخوایم خیلی ساده تعریفش کنیم:

هر Action فقط مسئول انجام یک عملیات مشخص در برنامه است.

دقت کن که گفتیم یک عملیات.

توی Service معمولاً کلاس‌ها رو بر اساس موجودیت‌ها (Entity) نام‌گذاری می‌کنیم.

مثلاً:

  • UserService

  • OrderService

  • ProductService

اما در Action معمولاً اسم کلاس‌ها بر اساس کاری که قرار است انجام شود انتخاب می‌شود.

مثلاً:

  • RegisterUserAction

  • CreateOrderAction

  • CancelOrderAction

  • PublishPostAction

  • UpdateProfileAction

وقتی اسم این کلاس‌ها رو می‌بینی، بدون اینکه حتی داخلشون رو باز کنی، دقیقاً می‌دونی هر کدوم چه کاری انجام میدن.

همین موضوع باعث میشه ساختار پروژه خواناتر و پیدا کردن منطق هر عملیات ساده‌تر بشه.


Action یعنی یک Use Case

اینجا یه نکته خیلی مهم وجود داره که معمولاً باعث سوءتفاهم میشه.

Action به معنی «یک متد از Service» نیست.

بلکه در بسیاری از معماری‌ها، نماینده‌ی یک Use Case یا یک عملیات مشخص در برنامه است.

مثلاً این‌ها Use Case هستن:

  • ثبت‌نام کاربر

  • ایجاد سفارش

  • لغو سفارش

  • انتشار مقاله

  • ثبت دیدگاه

اما این‌ها معمولاً Use Case محسوب نمیشن:

  • ارسال ایمیل

  • ثبت لاگ

  • ساخت پروفایل

  • آپلود فایل

این‌ها فقط بخشی از یک فرآیند بزرگ‌تر هستن.

مثلاً هنگام ثبت‌نام کاربر ممکنه لازم باشه:

  • کاربر داخل دیتابیس ذخیره بشه.

  • پروفایل اولیه ساخته بشه.

  • ایمیل خوش‌آمدگویی ارسال بشه.

  • یه لاگ ثبت بشه.

همه این مراحل، کنار هم یک Use Case به اسم ثبت‌نام کاربر رو تشکیل میدن.


آیا Action جایگزین Service است؟

اینجا می‌رسیم به مهم‌ترین سؤال این مقاله.

شاید بعد از خوندن توضیحات بالا با خودت بگی:

«پس از این به بعد فقط Action می‌سازیم و Service رو کنار می‌ذاریم.»

پاسخ کوتاه اینه:

نه.

Action قرار نیست Service رو حذف کنه.

همون‌طور که Service هم قرار نبود Repository رو حذف کنه.

هر کدوم برای حل یک مسئله متفاوت به وجود اومدن.

در ادامه، با یک مثال واقعی می‌بینیم که چطور می‌تونیم یک فرآیند ثبت‌نام رو با استفاده از Action Pattern پیاده‌سازی کنیم و بعد بررسی کنیم که در چه شرایطی Action به‌تنهایی کافی است و در چه شرایطی وجود یک Service همچنان بهترین انتخاب خواهد بود.


اولین Action خودمان را بسازیم

حالا که با مفهوم Action آشنا شدیم، وقتشه ببینیم توی پروژه چطور ازش استفاده کنیم.

فرض کن توی سایتت امکان انتشار مقاله وجود داره.

وقتی نویسنده روی دکمه «انتشار» کلیک می‌کنه، چند اتفاق باید بیفته:

  • وضعیت مقاله از draft به published تغییر کنه.

  • زمان انتشار ذخیره بشه.

  • یک لاگ ثبت بشه.

  • (در آینده شاید) به اعضای سایت اعلان ارسال بشه.

همه این‌ها کنار هم، یک عملیات مشخص رو تشکیل میدن:

انتشار مقاله

این دقیقاً همون چیزیه که می‌تونه یک Action باشه.


قدم اول؛ ساخت Action

برای شروع، یک پوشه جدید داخل app ایجاد می‌کنیم.

app/
└── Actions/

حالا داخل این پوشه، فایل زیر رو می‌سازیم:

app/Actions/PublishPostAction.php

کد اولیه کلاس:

<?php

namespace App\Actions;

class PublishPostAction
{
    public function execute()
    {
        //
    }
}

فعلاً هیچ کاری انجام نمیده.

فقط داریم ساختار Action رو آماده می‌کنیم.

البته اینم بگم که در لاراول یک روش رایج دیگه هم برای نوشتن Action وجود داره.

به جای متدی مثل execute() می‌تونیم از متد جادویی __invoke() استفاده کنیم.

در این حالت، کلاس ما قابل فراخوانی مستقیم میشه:

<?php

namespace App\Actions;

class PublishPostAction
{
    public function __invoke(int $postId)
    {
        //
    }
}

و هنگام استفاده:

$publishPostAction($postId);

به جای:

$publishPostAction->execute($postId);

هر دو روش کاملاً قابل قبول هستند.

بعضی تیم‌ها به دلیل اینکه هر Action فقط یک عملیات مشخص انجام میده، ترجیح می‌دهند از __invoke() استفاده کنند. بعضی دیگر هم برای خوانایی بیشتر از متدی مثل execute() یا handle() استفاده می‌کنند.

مهم‌تر از اسم متد، اینه که هر Action یک مسئولیت مشخص داشته باشه.


قدم دوم؛ وابستگی‌ها را تزریق کنیم

توی مقاله Repository گفتیم که بهتره عملیات مربوط به دیتابیس داخل Repository باشه.

پس اینجا هم مستقیم سراغ مدل Post نمیریم.

فرض کنیم قبلاً یک Repository برای مقاله‌ها ساختیم.

<?php

namespace App\Actions;

use App\Repositories\Contracts\PostRepositoryInterface;
use Illuminate\Support\Facades\Log;

class PublishPostAction
{
    public function __construct(
        protected PostRepositoryInterface $postRepository
    ) {}

    public function execute(int $postId)
    {
        //
    }
}

اینجا هنوز هیچ کاری انجام ندادیم.

فقط به Action گفتیم اگه خواستی با مقاله‌ها کار کنی، از Repository کمک بگیر.

نکته: اگه در پروژه‌ات از Repository استفاده می‌کنی، Action می‌تونه برای کار با داده‌ها از Repository کمک بگیره. اینطوری مسئولیت‌ها همچنان از هم جدا می‌مونن. البته در پروژه‌های کوچیک‌تر، استفاده مستقیم از مدل‌ها هم کاملاً قابل قبوله.


قدم سوم؛ منطق انتشار مقاله

حالا عملیات اصلی رو پیاده‌سازی می‌کنیم.

<?php

namespace App\Actions;

use App\Repositories\Contracts\PostRepositoryInterface;
use Illuminate\Support\Facades\Log;

class PublishPostAction
{
    public function __construct(
        protected PostRepositoryInterface $postRepository
    ) {}

    public function execute(int $postId)
    {
        $post = $this->postRepository->findById($postId);

        $this->postRepository->update($post, [
            'status' => 'published',
            'published_at' => now(),
        ]);

        Log::info('مقاله منتشر شد.', [
            'post_id' => $post->id,
        ]);

        return $post->fresh();
    }
}

اگه به این کلاس دقت کنی، می‌بینی که فقط یک مسئولیت داره.

هر بار که این کلاس اجرا میشه، فقط یک سؤال رو جواب میده:

"چطور یک مقاله را منتشر کنم؟"

نه چیزی بیشتر.

نه چیزی کمتر.


استفاده داخل کنترلر

حالا کنترلر خیلی ساده میشه.

<?php

namespace App\Http\Controllers;

use App\Actions\PublishPostAction;

class PostController extends Controller
{
    public function publish(
        int $id,
        PublishPostAction $publishPostAction
    ) {
        $publishPostAction->execute($id);

        return back()->with(
            'success',
            'مقاله با موفقیت منتشر شد.'
        );
    }
}

تقریباً تمام منطق انتشار از کنترلر خارج شده.

کنترلر فقط سه کار انجام میده:

  • درخواست رو دریافت می‌کنه.

  • Action مناسب رو اجرا می‌کنه.

  • پاسخ مناسب رو برمی‌گردونه.

دقیقاً همون مسئولیتی که باید داشته باشه.


تفاوتش با Service چیست؟

احتمالاً اینجا یه سؤال برات پیش اومده.

این کلاس:

PublishPostAction

چه فرقی با این داره؟

PostService

فرض کن از قبل چنین سرویسی داشتی.

class PostService
{
    public function publish(int $id) { /* ... */ }
    public function unpublish(int $id) { /* ... */ }
    public function archive(int $id) { /* ... */ }
    public function restore(int $id) { /* ... */ }
    public function duplicate(int $id) { /* ... */ }
    public function updateSeo(int $id) { /* ... */ }
    public function generateSlug(int $id) { /* ... */ }
}

این ساختار ذاتاً اشتباه نیست.

اتفاقاً توی خیلی از پروژه‌ها کاملاً قابل قبوله.

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

هر بار که قابلیت جدیدی مربوط به مقاله‌ها اضافه میشه، احتمال زیادی وجود داره که اولین جایی که به ذهنمون برسه همین PostService باشه.

بعد از مدتی این کلاس تبدیل به محل انجام همه عملیات‌های مربوط به مقاله میشه.

در نتیجه:

  • پیدا کردن منطق هر عملیات سخت‌تر میشه.

  • کلاس بزرگ‌تر میشه.

  • مسئولیت‌های مختلف داخل یک فایل جمع میشن.

در مقابل، اگه هر عملیات مهم پروژه Action خودش رو داشته باشه، ساختار پروژه خواناتر میشه.

مثلاً:

app/
└── Actions/
    ├── PublishPostAction.php
    ├── ArchivePostAction.php
    ├── RestorePostAction.php
    ├── DuplicatePostAction.php
    └── UpdatePostSeoAction.php

فقط با نگاه کردن به اسم فایل‌ها، تقریباً میشه فهمید هر کدوم چه کاری انجام میدن.

دیگه لازم نیست وارد یک کلاس بزرگ بشی و بین ده‌ها متد مختلف دنبال عملیات مورد نظرت بگردی.


یک نکته مهم

اینجا ممکنه این سؤال پیش بیاد:

پس یعنی از این به بعد برای هر کاری یک Action بسازیم؟

هنوز نه.

چون تا اینجا فقط با یک عملیات ساده سروکار داشتیم.

اما فرض کن فردا قرار باشه فرآیند ثبت سفارش یک فروشگاه اینترنتی رو پیاده‌سازی کنیم.

ثبت سفارش فقط تغییر وضعیت یک رکورد نیست.

ممکنه شامل این مراحل باشه:

  • بررسی موجودی کالا

  • محاسبه تخفیف

  • محاسبه مالیات

  • ایجاد سفارش

  • ثبت تراکنش

  • کاهش موجودی انبار

  • ارسال پیامک

  • ارسال ایمیل

  • ثبت لاگ

آیا باز هم همه این منطق باید داخل یک Action قرار بگیره؟

یا اینجا وقتشه که دوباره از Service کمک بگیریم؟

دقیقاً اینجاست که مرز بین Action و Service مشخص میشه.

در ادامه، با همین مثال فروشگاه می‌بینیم که Action و Service نه‌تنها رقیب هم نیستن، بلکه در کنار هم می‌تونن یک ساختار تمیز، قابل توسعه و حرفه‌ای بسازن.


یک سناریوی واقعی؛ فروشگاه اینترنتی

تا اینجا مثال‌های نسبتاً ساده‌ای دیدیم.

اما بیایم یه پروژه واقعی‌تر رو تصور کنیم.

فرض کن داری یه فروشگاه اینترنتی مثل دیجی‌کالا توسعه میدی.

کاربر روی دکمه «ثبت سفارش» کلیک می‌کنه.

به نظرت چه اتفاق‌هایی باید بیفته؟

شاید در نگاه اول فکر کنیم فقط یه رکورد داخل جدول orders ذخیره میشه.

اما در عمل، معمولاً مراحل بیشتری وجود داره.

مثلاً:

  • موجودی کالاها بررسی بشه.

  • قیمت نهایی محاسبه بشه.

  • تخفیف‌ها اعمال بشن.

  • سفارش داخل دیتابیس ذخیره بشه.

  • موجودی انبار کم بشه.

  • تراکنش ثبت بشه.

  • ایمیل تأیید سفارش ارسال بشه.

  • پیامک ارسال بشه.

  • یک اعلان برای پنل مدیریت ثبت بشه.

  • یه لاگ ثبت بشه.

همه این مراحل، فقط با یک دکمه انجام میشن.


آیا همه این‌ها باید داخل مثلاً CreateOrderAction باشند؟

اگه بخوایم دقیقاً با همون ذهنیت بخش قبل جلو بریم، شاید به این نتیجه برسیم که:

CreateOrderAction

باید همه این مراحل رو انجام بده.

یعنی چیزی شبیه این:

public function execute(array $data)
{
    $this->checkInventory();
    $this->calculateDiscount();
    $this->createOrder();
    $this->reduceInventory();
    $this->createTransaction();
    $this->sendEmail();
    $this->sendSms();
    $this->notifyAdmin();
    $this->writeLog();
}

از نظر فنی این کد کاملاً قابل اجراست.

اما...

اگه فردا تصمیم بگیری:

  • ارسال پیامک رو حذف کنی.

  • سیستم ایمیل رو تغییر بدی.

  • پرداخت اقساطی اضافه کنی.

  • یا سفارش از طریق API ثبت بشه.

کم‌کم همین Action هم شروع می‌کنه به بزرگ شدن.

یعنی دقیقاً همون مشکلی که اول مقاله درباره Service گفتیم، ممکنه این بار برای Action هم اتفاق بیفته.

پس نتیجه می‌گیریم:

Action هم اگه بیش از حد مسئولیت بگیره، کیفیتش پایین میاد.


پس راه‌حل چیه؟

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

اما این بار نقشش با قبل فرق داره.

Service قرار نیست محل نوشتن همه منطق پروژه باشه.

بلکه در این سناریو، نقشش اینه که چند عملیات مستقل رو با هم هماهنگ کنه.


تقسیم مسئولیت‌ها

بیایم همین فرآیند ثبت سفارش رو به چند بخش کوچک تقسیم کنیم.

  • CheckInventoryAction

  • CalculateDiscountAction

  • CreateOrderAction

  • ReduceInventoryAction

  • CreateTransactionAction

  • SendOrderEmailAction

  • SendOrderSmsAction

هر Action فقط یک مسئولیت داره.

مثلاً:

  • CreateOrderAction فقط سفارش رو داخل دیتابیس ایجاد می‌کنه.

  • ReduceInventoryAction فقط موجودی انبار رو کم می‌کنه.

  • SendOrderEmailAction فقط ایمیل تأیید سفارش رو ارسال می‌کنه.

هر کدوم از این کلاس‌ها مستقل هستن و فقط یک مسئولیت مشخص دارن. نتیجه اینه که فرآیند ثبت سفارش به چند بخش کوچک و قابل مدیریت تقسیم میشه؛ بخش‌هایی که فهمیدنش، تغییر دادنش و تست کردنشون خیلی راحت‌تر از زمانی هست که همه منطق داخل یک کلاس بزرگ قرار گرفته باشه.


حالا Service چه کاری انجام میده؟

حالا یه کلاس به اسم

OrderService

داریم.

اما این بار دیگه خبری از ده‌ها متد مختلف نیست.

فرض کنیم فقط یک مسئولیت داره:

هماهنگ کردن فرآیند ثبت سفارش.

مثلاً:

class OrderService
{
    public function __construct(
        protected CheckInventoryAction $checkInventory,
        protected CalculateDiscountAction $calculateDiscount,
        protected CreateOrderAction $createOrder,
        protected ReduceInventoryAction $reduceInventory,
        protected CreateTransactionAction $createTransaction,
        protected SendOrderEmailAction $sendEmail,
        protected SendOrderSmsAction $sendSms
    ) {
    }

    public function placeOrder(array $data)
    {
        $this->checkInventory->execute($data);
        $this->calculateDiscount->execute($data);
        $order = $this->createOrder->execute($data);
        $this->reduceInventory->execute($order);
        $this->createTransaction->execute($order);
        $this->sendEmail->execute($order);
        $this->sendSms->execute($order);

        return $order;
    }
}

اگه دقت کنی، این کلاس تقریباً هیچ منطق پیچیده‌ای نداره.

فقط داره میگه:

اول این کار رو انجام بده... بعد اون یکی... بعد این یکی...

در واقع Service تبدیل شده به رهبر ارکستر :)

خودش ساز نمی‌زنه.

فقط مشخص می‌کنه هر بخش چه زمانی اجرا بشه.


Repository این وسط کجاست؟

یه سؤال دیگه.

الان Repository چه نقشی داره؟

همون نقشی که از اول داشت.

مثلاً داخل

CreateOrderAction

می‌تونیم از Repository استفاده کنیم.

class CreateOrderAction
{
    public function __construct(
        protected OrderRepositoryInterface $orders
    ) {
    }

    public function execute(array $data)
    {
        return $this->orders->create($data);
    }
}

یعنی Action خودش مستقیماً با دیتابیس صحبت نمی‌کنه.

برای این کار از Repository کمک می‌گیره.


حالا تصویر کلی پروژه خیلی واضح‌تره

اگه بخوایم معماری این فرآیند رو به صورت ساده رسم کنیم، چیزی شبیه این میشه:

Controller
      │
      ▼
OrderService
      │
      ├────────────────────────────┐
      ▼                            ▼
CheckInventoryAction         CreateOrderAction
      │                            │
      ▼                            ▼
ProductRepository          OrderRepository
      │                            │
      └──────────────┬─────────────┘
                     ▼
                 Database

البته Service فقط به این دو Action محدود نمیشه.

ممکنه Actionهای دیگه‌ای مثل:

  • CalculateDiscountAction

  • SendOrderEmailAction

  • SendOrderSmsAction

  • ReduceInventoryAction

هم داخلش اجرا بشن.

برای اینکه نمودار شلوغ نشه، فقط مهم‌ترین بخش‌ها رو نشون دادیم.


چه چیزی به دست آوردیم؟

اگه یه روز مدیر پروژه بگه:

«از این به بعد، بعد از ثبت سفارش، یک نوتیفیکیشن برای کاربر ارسال بشه.»

فقط کافیه:

SendPushNotificationAction

رو اضافه کنیم.

بعد داخل Service فقط یک خط بنویسیم.

$this->sendPushNotification->execute($order);

نه لازم شد CreateOrderAction رو تغییر بدیم.

نه Controller.

همه کلاس‌ها همچنان مسئولیت خودشون رو دارن.


حالا تفاوت نقش‌ها کاملاً مشخصه

تا اینجا می‌تونیم وظیفه هر لایه رو خیلی ساده این‌طوری تعریف کنیم:

لایه Controller مسئولیت دریافت درخواست و برگرداندن پاسخ

لایه Service مسئولیت هماهنگ کردن یک فرآیند بزرگ (در سناریوهایی که نیاز به هماهنگی چند Action داریم)

لایه Action مسئولیت انجام یک عملیات مشخص و مستقل

لایه Repository مسئولیت کار با دیتابیس.

اگه این چهار نقش رو درست از هم جدا کنیم، معمولاً پروژه با بزرگ‌تر شدن هم ساختار خودش رو حفظ می‌کنه و پیدا کردن محل مناسب برای اضافه کردن قابلیت‌های جدید خیلی راحت‌تر میشه.


چه زمانی Action نسازیم؟

بعد از خوندن این مقاله، شاید وسوسه بشی که از این به بعد برای هر کاری یک Action بسازی.

اما مثل Repository و Service، اینجا هم قرار نیست از یک الگو در همه جای پروژه استفاده کنیم.

هدف Action، ساده‌تر کردن ساختار پروژه هست؛ نه اینکه برای هر چند خط کد، یک کلاس جدید ایجاد کنیم.

مثلاً اگه فقط می‌خوایم وضعیت یک فیلد را تغییر بدیم و این منطق قرار نیست جای دیگه‌ای استفاده بشه، معمولاً نیازی به Action جداگانه نیست.

یا اگر یک عملیات خیلی ساده است، مثل:

$user->update([
    'is_active' => true
]);

ساختن یک Action فقط برای همین چند خط، احتمالاً ارزش پیچیدگی اضافه رو نداره.

اما اگه یک فرآیند چند مرحله‌ای داری، مثل:

  • ثبت سفارش

  • ایجاد کاربر جدید

  • پردازش پرداخت

  • لغو سفارش و برگرداندن موجودی

که شامل چند تصمیم، چند وابستگی یا چند عملیات مختلف میشه، استفاده از Action می‌تونه ساختار کد رو مرتب‌تر کنه.

به زبان ساده:

اگه یک کار مشخص، مستقل و قابل استفاده مجدد داری، Action انتخاب خوبیه.

اگه فقط چند خط کد ساده داری که جای دیگه‌ای استفاده نمیشه، احتمالاً Action اضافه‌ست.


Action زمانی ارزشمند می‌شود که...

معمولاً وقتی یکی از شرایط زیر وجود داشته باشه، Action می‌تونه انتخاب خوبی باشه:

  • یک عملیات مشخص و مستقل داری.

  • همان عملیات ممکنه از چند جای مختلف پروژه استفاده بشه.

  • منطق اون عملیات کم‌کم در حال بزرگ شدنه.

  • می‌خوای تست کردن اون بخش ساده‌تر باشه.

  • دوست داری مسئولیت‌ها تا جای ممکن از هم جدا باشند.

در چنین شرایطی، Action می‌تونه کد رو خواناتر و قابل نگهداری‌تر کنه.


چند اشتباه رایج

مثل هر الگوی دیگه‌ای، استفاده نادرست از Action هم می‌تونه نتیجه برعکس بده.

۱. ساختن Action برای هر خط کد

گاهی دیده میشه برای ساده‌ترین کارها هم Action ساخته میشه.

مثلاً:

  • UpdateUserNameAction

  • UpdateUserEmailAction

  • UpdateUserPhoneAction

در حالی که شاید همه این‌ها بخشی از یک عملیات بزرگ‌تر به اسم:

UpdateUserProfileAction

باشن.

اینجا تقسیم‌بندی بیش از حد، فقط تعداد فایل‌ها رو زیاد می‌کنه.


۲. انجام مستقیم کارهای دیتابیس داخل Action

گاهی Action تبدیل میشه به جایی برای نوشتن Queryهای Eloquent.

مثلاً:

User::where(...)->update(...);

اگه معماری پروژه‌ات بر پایه Repository هست، بهتره Action برای دسترسی به داده‌ها از Repository کمک بگیره.

این باعث میشه مسئولیت‌ها همچنان از هم جدا بمونن.


۳. تبدیل شدن Action به یک Service دیگر

اگه داخل یک Action ده‌ها مرحله مختلف انجام میدی، شاید وقتشه دوباره به ساختارت نگاه کنی.

مثلاً:

  • ایجاد سفارش

  • کاهش موجودی

  • ثبت تراکنش

  • ارسال ایمیل

  • ارسال پیامک

  • ثبت لاگ

  • ارسال اعلان

اگه همه این‌ها داخل یک Action باشن، احتمالاً دوباره به همون نقطه‌ای برگشتیم که اول مقاله درباره‌ش صحبت کردیم.


۴. استفاده از Action فقط چون مد شده است!

یکی از اشتباه‌های رایج اینه که صرفاً چون در پروژه‌های معروف یا پکیج‌های مختلف از Action استفاده شده، ما هم بدون نیاز واقعی ازش استفاده کنیم.

هیچ معماری‌ای به خودی خود خوب یا بد نیست.

مهم اینه که آیا واقعاً مسئله‌ای از پروژه تو رو حل می‌کنه یا نه.


آیا استفاده از این الگوها اجباری است؟

نه.

شاید این مهم‌ترین نکته کل این مجموعه مقاله‌ها باشه.

اگه پروژه کوچیکه، دو سه صفحه بیشتر نداره و فقط خودت روش کار می‌کنی، احتمالاً استفاده از همه این لایه‌ها فقط پیچیدگی اضافه ایجاد می‌کنه.

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

به همین خاطر، خیلی از تیم‌ها این الگوها رو نه به خاطر «مد بودن»، بلکه برای نگهداری بهتر پروژه در بلندمدت انتخاب می‌کنن.


حرف آخر

توی این سه مقاله، قدم‌به‌قدم با سه الگوی پرکاربرد در پروژه‌های لاراول آشنا شدیم.

اول دیدیم که Repository چطور منطق مربوط به دیتابیس رو از کنترلر جدا می‌کنه.

بعد با Service Layer آشنا شدیم و فهمیدیم که منطق بیزینسی بهتره داخل یک لایه مستقل قرار بگیره.

و در نهایت، دیدیم که Action می‌تونه عملیات‌های مشخص و مستقل رو به کلاس‌های کوچک‌تر و قابل استفاده مجدد تبدیل کنه.

نکته مهم اینه که این سه الگو قرار نیست جای همدیگه رو بگیرن.

هر کدوم برای حل یک مسئله متفاوت به وجود اومدن و وقتی در جای درست خودشون استفاده بشن، نتیجه‌شون کدی تمیزتر، خواناتر و قابل نگهداری‌تره.

خلاصه اینکه:

مهم‌ترین بخش معماری، انتخاب الگو نیست؛ تشخیص درست مسئله است. اول باید ببینی پروژه به چه مشکلی خورده و بعد راه‌حل مناسب رو انتخاب کنی. معماری خوب، یعنی انتخاب ساده‌ترین راه‌حلی که مسئله امروز پروژه‌ات رو حل می‌کنه؛ نه استفاده از بیشترین تعداد الگوها.


اگه جایی سوالی داری، نکته‌ای به ذهنت رسید، یا حتی اگه فکر میکنی یه قسمتی از این مقاله رو میشه بهتر نوشت، حتماً تو نظرات بهم بگو. همه با هم یاد میگیریم! 🙏


نکته پایانی: این مقاله سومین بخش از سری مقاله‌های «معماری تمیز در لاراول» بود. توی مقاله‌های قبلی با Repository و Service آشنا شدیم و حالا Action رو هم یاد گرفتیم. با اینها، میتونی پروژه‌هات رو به‌صورت حرفه‌ای و تمیز مدیریت کنی. موفق باشی 🚀

ارسال دیدگاه

دیدگاه و یا پرسش خود را برای ما ارسال کنید.

وارد شوید

برای ارسال دیدگاه یا پرسش خود ابتدا وارد سایت شوید

ورود یا ثبت نام

دیدگاه کاربران

هنوز دیدگاه یا پرسشی ایجاد نشده است :/

تازه‌ترین نوشته‌ها

دیدن همه

تجربه‌ها، دیدگاه‌ها و نکات الهام‌بخشی که با شما به اشتراک می‌گذاریم.