توسعه نرم‌افزار

معماری تمیز در لاراول

نویسنده: Hadi ZareZadeh۴ خرداد ۱۴۰۵۵۳۴۱ بازدید
معماری تمیز در لاراول

اولین بار که «معماری تمیز» را در مورد لاراول شنیدم، پوشه‌بندی سازمانی به سبک جاوا را تصور کردم و تیمی از معماران که یک هفته درباره نام یک اینترفیس بحث می‌کنند. بعد به‌هرحال یک پروژه را به آن شکل تحویل دادم و چیزی جا افتاد: نکته، تشریفات نیست. نکته، ارزان‌تر کردن تغییر فردا نسبت به میان‌بُر امروز است.

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

واقعاً چه مسئله‌ای را حل می‌کنیم؟

مسئله اصلی هم‌بستگی است. وقتی لایه HTTP، لایه پایگاه داده و قواعد کسب‌وکارتان درهم تنیده‌اند، هر تغییر پرریسک است. معماری تمیز مرزهایی می‌کشد تا:

  • قواعد کسب‌وکار از درخواست‌های HTTP یا Eloquent چیزی ندانند.
  • کنترلرها منطق کسب‌وکار نداشته باشند — آن‌ها HTTP را به فراخوانی اپلیکیشن ترجمه می‌کنند.
  • بتوانید بخش مهم را بدون راه‌اندازی کل فریم‌ورک تست کنید.
معماری مجموعه تصمیماتی است که برگرداندنشان گران است. معماری تمیز این تصمیم‌ها را به‌جای تصادفی، صریح می‌کند.

لایه‌هایی که واقعاً استفاده می‌کنم

دیاگرام آنکل باب را عیناً کپی نمی‌کنم. در یک اپلیکیشن لاراول، این ساختاری است که در چند کدبیس تولیدی برایم جواب داده:

لایهمسئولیتمحل قرارگیری
HTTPمسیریابی، اعتبارسنجی، احراز هویت، قالب‌بندی پاسخapp/Http/
اپلیکیشنهماهنگ‌سازی: «این کاربر را در این دوره ثبت‌نام کن»app/Domain/*/Actions/
دامنهقواعد کسب‌وکار، ناوردایی‌ها، سرویس‌های دامنهapp/Domain/
زیرساختEloquent، APIهای بیرونی، ایمیل، صف‌هاapp/Domain/*/Services/، مدل‌ها

اکشن‌ها: الگوی اسب‌بارکش

اکشن یک کلاس تک‌منظوره است که یک کار را در لایه اپلیکیشن انجام می‌دهد. به‌جای یک متد کنترلر ۲۰۰ خطی، چیزی متمرکز و قابل تست می‌گیرید:

namespace App\Domain\Enrollment\Actions;

final class EnrollUserInCourse
{
    public function __construct(
        private EnrollmentValidator $validator,
        private EnrollmentRepository $repository,
    ) {}

    public function execute(User $user, Course $course): Enrollment
    {
        $this->validator->ensureEligible($user, $course);

        return $this->repository->create($user, $course);
    }
}

کنترلر نازک می‌شود — تقریباً کسل‌کننده، که هدف همین است:

public function store(EnrollRequest $request, EnrollUserInCourse $action)
{
    $enrollment = $action->execute(
        $request->user(),
        Course::findOrFail($request->course_id)
    );

    return EnrollmentResource::make($enrollment);
}

نگه داشتن Eloquent در جای خودش

هنوز از Eloquent استفاده می‌کنم. از روز اول برای هر مدل یک انتزاع repository نمی‌سازم. اما یک خط می‌کشم: مدل‌های Eloquent اشیای پایداری‌اند، نه اشیای دامنه با منطق کسب‌وکار چاشنی‌شده روی آن‌ها. وقتی یک مدل شروع به رشد متدهایی مثل canUserEnroll() می‌کند که به سه مدل دیگر وابسته‌اند، این نشانه‌ای است که یک سرویس دامنه یا اکشن استخراج کنید.

سرویس‌های دامنه برای قواعد عرضی

وقتی یک قاعده چند موجودیت یا بررسی بیرونی را درگیر می‌کند، یک سرویس دامنه کارها را خوانا نگه می‌دارد:

final class EnrollmentValidator
{
    public function ensureEligible(User $user, Course $course): void
    {
        if ($course->isFull()) {
            throw new CourseFullException();
        }

        if ($user->alreadyEnrolledIn($course)) {
            throw new AlreadyEnrolledException();
        }
    }
}

رویدادها و اثرات جانبی

اکشن‌ها باید کار اصلی را همگام انجام دهند و رویدادها را برای اثرات جانبی منتشر کنند. ارسال ایمیل، به‌روزرسانی تحلیل‌ها، اطلاع‌رسانی به مدرسان — این‌ها به listenerها تعلق دارند، نه به خود اکشن. این کار اکشن را خوانا نگه می‌دارد و تست اثرات جانبی را در انزوا آسان می‌کند.

// داخل اکشن، بعد از ساخت ثبت‌نام:
event(new UserEnrolled($enrollment));

چیزهایی که عمداً انجام نمی‌دهم

  • اینترفیس repository برای همه چیز از روز اول. انتزاع را وقتی اضافه کنید که یک پیاده‌سازی دوم یا نیاز واقعی به تست دارید، نه پیشگیرانه.
  • DTO برای هر پارامتر متد. گاهی یک User و یک Course کافی‌اند. به‌خاطر خود انتزاع، انتزاع نسازید.
  • «موجودیت‌های» جدا که آینه مدل‌های Eloquent باشند. مگر اینکه DDD واقعی با مدل‌های دامنه غنی انجام می‌دهید، تکرار لایه داده اتلاف است.

اشتباهات رایج

  • مهندسی بیش از حد پروژه‌های کوچک. یک صفحه فرود به لایه اکشن نیاز ندارد. این را وقتی به کار ببرید که پیچیدگی توجیهش کند.
  • گذاشتن اعتبارسنجی در اکشن‌ها به‌جای form request. اعتبارسنجی سطح HTTP در form request می‌ماند؛ اعتبارسنجی قاعده کسب‌وکار در سرویس دامنه.
  • تست همه چیز از طریق HTTP. اکشن‌ها و سرویس‌های دامنه را مستقیم unit-test کنید. تست feature را برای مسیرهای یکپارچه‌سازی نگه دارید.

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

  • وقتی یک متد کنترلر از حدود ۳۰ خط فراتر رفت یا بیش از یک دلیل برای تغییر داشت، شروع به استخراج اکشن کنید.
  • بر اساس دامنه (Enrollment، Payment، Catalog) سازماندهی کنید، نه بر اساس لایه (Actions، Services در سطح بالا).
  • قبل از بازآرایی کنترلر، تست اکشن را بنویسید. تست تور ایمنی شماست.
  • قواعد مرزی را در یک ADR یا README کوتاه مستند کنید تا اعضای جدید تیم به کنترلرهای چاق برنگردند.

جمع‌بندی

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