برنامه‌‌های لاراول در مقیاس بزرگ

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

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

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

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

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

ساختار پیش‌فرض لاراول شبیه ساختار زیر است:

|- app/
   |- Console/
      |- Commands/
   |- Events/
   |- Exceptions/
   |- Http/
      |- Controllers/
      |- Middleware/
   |- Jobs/
   |- Listeners/
   |- Providers/
   |- User.php
|- database/
   |- factories/
   |- migrations/
   |- seeders
|- config/
|- routes/
|- resources/
   |- assets/
   |- lang/
   |- views/

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

|- app/
   |- Console/
      |- Commands/
   |- Events/
   |- Exceptions/
   |- Http/
      |- Controllers/
      |- Middleware/
   |- Jobs/
   |- Listeners/
   |- Models/
   |- Presenters/
   |- Providers/
   |- Repositories/
   |- Services/
   |- Transformers/
   |- Validators/
|- database/
   |- factories/
   |- migrations/
   |- seeders
|- config/
|- routes/
|- resources/
   |- assets/
   |- lang/
   |- views/

همان طور که می‌بینید این پروژه از سازماندهی مناسبی برخوردار هست. حالا، از نزدیگ نگاهی به پوشه Models  بیاندازیم.

|- app/
  |- Models/
     |- User.php
     |- Role.php
     |- Permission.php
     |- Merchant.php
     |- Store.php
     |- Product.php
     |- Category.php
     |- Tag.php
     |- Client.php
     |- Delivery.php
     |- Invoice.php
     |- Wallet.php
     |- Payment.php
     |- Report.php

تا اینجا باید گفت که کار خوب پیش رفته است. همچنین پوشه Services نیز وجود دارد که با منطق برنامه سر و کار دارد. پوشه های Repository ، Transforms و Validators نیز وجود دارند که هرکدام تعداد خاصی کلاس در آنها تعریف شده است. اما باید توجه داشته باشیم که کار کردن روی یک موجودیت (شخص – پست - رویداد)/ مدل احتیاج به جستجو در چندیدن دایرکتوری و فایل مختلف دارد که برای بعضی‌ها خسته کننده می‌باشد و  البته برای برخی دیگر خوب است. 

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

  • این برنامه تک لایه است.
  • حفظ یکپارچگی این برنامه (برای برخی توسعه دهندگان) سخت است.
  • بهره‌وری پایین است (همیشه باید به فکر ارتباط فی مابین اجزا برنامه باشید. )
  • تغییر مقیاس برنامه همیشه مشکل است.

راه حل مشخصا، میکروسرویس است. حتی در مواردی که ما از SOA (معماری سرویس‌گرا) استفاده میکنیم؛ احتیاج داریم که برنامه تک لایه خود را به قسمت های مستقل کوچکتری تقسیم کنیم تا بتوانیم بعدا آن قسمت‌ها را جداگانه گسترش دهیم.

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

معمولا تقسیم یک سرویس احتیاج به دو مرحله دارد.

  • انتقال فرزندان سرویس (مدل‌ها،ریپوزیتوری‌ها،ترانسفورم‌ها و ...) به یک برنامه میکروسرویس php جدید.
  • ریفکتور کردن فراخوانی‌های تابع سرویس برای ریدایرکت کردن مقصد به میکروسرویس(برای مثال ساختن درخواست Http)

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

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

البته مورد آخر اهمیت بیشتری دارد. به‌این دلیل که توسعه دهندگان تازه کار نمی‌توانند در زمان کوتاهی مفهوم کل برنامه را درک کنند . البته مدیر پروژه به این موضوع توجهی ندارد و زمان زیادی در اختیار توسعه دهندگان تازه کار قرار نمی‌دهد. این موضوع باعث بوجود آمدن حالت Monkey Patching  می‌شود. و اشتباهاتی هنگام جابجایی کد صورت خواهد گرفت که این باعث سردرگمی بیشتر برای توسعه دهندگان بعدی خواهد شد.

خوشبختانه ما یک راه‌حل مناسب داریم – HMVC. به این صورت که کل برنامه به بخش های کوچکتری ، که هر کدام از این قسمت‌ها، پوشه‌بندی و فایل‌‌های مربوط به خود را دارد؛ مثل پوشه app/ که بوسیله اوتولودر composer.json بارگزاری می‌شود، به این شکل:

|- auth/
   |- Exceptions/
   |- Http/
   |- Listeners/
   |- Models/
   |- Presenters/
   |- Providers/
   |- Repositories/
   |- Services/
   |- Transformers/
   |- Validators/
|- merchant/
   |- Console/
   |- Events/
   |- Exceptions/
   |- Http/
   |- Jobs/
   |- Listeners/
   |- Models/
   |- Presenters/
   |- Providers/
   |- Repositories/
   |- Services/
   |- Transformers/
   |- Validators/
|- database/
   |- factories/
   |- migrations/
   |- seeders
|- config/
|- routes/
|- resources/
   |- assets/
   |- lang/
   |- views/

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

طراحی متاثر از دامنه کار (Domain Driven Design) به عنوان راه‌حلی دیگر

هیچ کدام از این راه‌حل‌ها کامل نیستند؛ هرکدام دارای مزایا و معایب خاص خود هستند. هر کسی ممکن است یکی از روش ها را برای خود ترجیح دهد. من نمی‌خواهم در اینجا DDD را مورد بحث قرار دهم . Developerul DeLaUnu  توضیحات مفصلی در این‌باره ارائه کرده‌است

از نظر من DDD (می‌تواند) پروژه لاراول شما در 4 بخش ساختاردهی ‌کند. (ویا سه بخش، نگاهی به اینجا بیاندازید) 

  • Application: معمولا پوشه‌های Controller و Middleware و Route را در بر می‌گیرد.
  • Domain: معمولا منطق برنامه را در بر می‌گیرد.(Model,Repository,Transformer, …)
  • Infrastructure: معمولا سرویس‌هایی مثل Logging و Email را شامل می‌شود.
  • Interface: شامل views,lang,assets

اگر این روش آسان به نظر می آید پس چرا پروژه خودمان را به این روش ساختاردهی نکنیم؟؟؟ و چرا از فضای نام استفاده نکنیم؟؟

|- app/
   |- Http/ (Application)
      |- Controllers/
      |- Middleware/
|- Domain/
   |- Models/
   |- Repositories/
   |- Presenters/
   |- Transformers/
   |- Validators/
   |- Services/
|- Infrastructure/
   |- Console/
   |- Exceptions/
   |- Providers/
   |- Events/
   |- Jobs/
   |- Listeners/
|- resources/ (Interface)
   |- assets/
   |- lang/
   |- views/
|- routes/
   |- api.php
   |- web.php

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

راستی آزمایی

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

|- app/
   |- Http/
      |- Controllers/
      |- Middleware/
   |- Providers/
   |- Account/
      |- Console/
      |- Exceptions/
      |- Events/
      |- Jobs/
      |- Listeners/
      |- Models/
         |- User.php
         |- Role.php
         |- Permission.php
      |- Repositories/
      |- Presenters/
      |- Transformers/
      |- Validators/
      |- Auth.php
      |- Acl.php
   |- Merchant/
   |- Payment/
   |- Invoice/
|- resources/
|- routes/

Auth.php و Acl.php فایل‌های سرویس هستندکه در پوشه app/Account قرار دارند. کنترلرها  فقط به این دو کلاس دسترسی دارند و فقط می توانند توابع این دو کلاس را فرا بخوانند. بقیه کلاس‌ها (خارج از دامنه) از بقیه کلاس های موجود در پوشه app/Account اطلاعی نخواهند داشت.  توابع درون این سرویس‌ها فقط نوع داده های ساده php مثل bool,array,string,int  و POPO(Plain Old PHP Object) را می پذیرند . توایع این سرویس ها نمونه کلاس‌ها را قبول ندارند. مثال:

...
public function register(array $attr) {
    ...
}
public function login(array $credentials) {
    ... 
}
public function logout() {
    ...
}
...

توجه داشته باشد تابع register یک آرایه را به عنوان مشخصات (Attributes)، به جای یک شیءUser  می‌پذیرد.  و این یک نکته بسیار مهم است چرا که کلاس دیگری که این تابع را فرا می خواند نباید از وجود مدل User اطلاع داشته باشد. و این یک قاعده اساسی در سراسر این ساختار است.

زمان تقسیم کد برنامه به چند قسمت

برنامه ما خیلی بزرگ شده و اکنون می‌خواهیم دامنه Account را به یک میکروسرویس جدا تبدیل کرده و انرا به یک سرویس OAuth تبدیل کنیم.

پس ما تنها کافی است قسمت های زیر را جابه جا کنیم –

|- Account/
   |- Console/
   |- Exceptions/
   |- Events/
   |- Jobs/
   |- Listeners/
   |- Models/
      |- User.php
      |- Role.php
      |- Permission.php
   |- Repositories/
   |- Presenters/
   |- Transformers/
   |- Validators/
   |- Auth.php
   |- Acl.php

و آن را یک برنامه جدید لاراول(و شاید Lumen) تبدیل کنیم-

|- app/
   |- Http/
      |- Controllers/
      | - Middleware/
   |- Account/
      |- Events/
      |- Jobs/
      |- Listeners/
      |- Models/
         |- User.php
         |- Role.php
         |- Permission.php
      |- Repositories/
      |- Presenters/
      |- Transformers/
      |- Validators/
      |- Auth.php
      |- Acl.php
|- routes/
|- resources/

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

ولی چه تغییری را باید در کدبیس اصلی اعمال کنیم؟

کافی است دو فایل Auth.php و Acl.php را نگه داشته؛ فقط کد درون توابع آنها را به درخواست های HTTP (و یا توابع دیگری مثل messaging) که مقصد آنها میکروسرویس تازه ساخته شده می باشد، تغییر دهیم.

...

public function login(array $credentials) {

 // change the code here

}

...مابقی برنامه را به حال خود می‌گذاریم و تغییری در آن انجام نمی‌دهیم.

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

|- app/
   |- Console/
   |- Exceptions/
   |- Http/
      |- Controllers/
      |- Middleware/
   |- Providers/
   |- Account/
      |- Auth.php
      |- Acl.php
   |- Merchant/
   |- Payment/
   |- Invoice/
|- resources/
|- routes/

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

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

بررسی نقاط منفی

همان طور که قبلا گفته بودم در مقابل ویژگی های مثبت این راه حل معایبی هم وجود دارد. و نقطه منفی این راه حل Migration ها می‌باشند. چرا که در ساختار پوشه‌بندی بالا (قبل از جدا‌سازی) تمامی migrationها درون پوشه database/migrations/ قرار دارند. اما وقتی که می‌خواهیم یک دامنه کار را جا به جا کنیم احتیاج است که فایل‌های میگریشن را نیز، با آن دامنه، به محل جدیدشان منتقل کنیم. ما هیچ نشانه گذاری خاصی برای اینکه بدانیم کدام میگریشن به کدام دامنه کاری مرتبط است، نداریم. لازم است به هر شکلی در هر فایل میگریشن علامت خاصی برای شناختن آن داشته باشیم. 

این نشانه میتواند پیشوندی با نام آن دامنه کاری باشد. برای مثال نام میگریشن را به xxxxxxxxx_create_account_users_table.phpبه‌جای xxxxxxxxx_create_users_table.php تغییرنام دهیم. همچنین می‌توانیم در صورت امکان نام جدول account_users را به users تغییر دهیم. جداسازی فایل‌های میگریشن ممکن است کمی خسته کننده باشد اما در صورتی که از پیشوند‌ و یا هر نوع نشانه گذاری دیگری استفاده کنیم قطعا این فرایند خیلی ساده تر خواهد شد. 

این ساختار هنوز در مرحله کارآزمایی و آزمایش است . من قصد دارم در آینده نزدیک یک پکیج لاراول بسازم. کار این پکیج، توانمند سازی دستورات artisan برای اتوماتیک کردن تولید فایل و پروسه جداسازی است. در آینده پس از ساختن این پکیج لینک آن در این مطلب قرار داده می‌شود. 

منبع

مقالات پیشنهادی

ساخت نصب کننده (Installer) برای پروژه لاراول

در این مقاله می خواهیم درمورد ساخت یک نصب کننده برای پروژه لاراول صحبت کنیم و با استفاده ازش بصورت اتوماتیک وظایف اولیه رو انجام بدیم.

اشتراک گذاری اطلاعات در اپلیکیشن لاراول/Vue

دریافت و ذخیره سازی اطلاعات در لاراول کار پیچیده ای نیست اما چطوری میشه اطلاعات لاراول رو در Vue استفاده کرد؟ 

ساخت چارت ها و نمودار ها در لاراول

Charts یک پکیج چندکاربردی برای ساخت نمودارها و چارت های جذاب در لاراول هست. بیش از 100 چارت مختلف و 13 کتابخانه ی چارت متفاوت برای انتخاب وجود دارد. ب...

ساخت پکیج لاراول 5 - قسمت آخر

در قسمت نهایی این سری آموزش ها ما می خواهیم یک بخش مهم و نادیده گرفته شده از توسعه ی پکیج ها رو بیان کنیم. تست واحد (Unit Testing) باعث میشه در حالی ک...