چطور از شلخته شدن مدل‌ها در لاراول جلوگیری کنیم؟

یکی از نصیحت‌ها در استفاده از فریم‌ورک‌هایی مثل Laravel و Ruby on Rails اینه: «کنترلرهای لاغر، مدل‌های چاق». امّا کافیه تلاش کرده باشین تو یه پروژه درست و حسابی این نصیحت رو به کار ببندین تا مواجه بشین با مدل‌های چند صد (بلکه چند هزار) خطی و با چند ده تابع که دست و پای هر برنامه‌نویسی رو برای تغییر می‌لرزونه. ولی این تنها راه برای ساختار دادن به پروژه نیست و میشه کاری کرد که همه کلاس‌ها لاغر و سلامت باشن. تو این نوشته، به روش‌های نوشتن مدل‌های خوش فرم و قابل نگهداری می‌پردازیم.


لعنت بر Active Record و دسترسی جادویی

کافیه یه class خالی در امتداد Model لاراول بنویسین و حالا دیگه دسترسی و پرس‌وجو (query) جدول هم‌نام این کلاس مثل آب خوردنه و یه instance از این کلاس، معادل یه ردیف دیتابیس میشه. خیلی خوبه، نه؟
برای شروع شاید خوب باشه، برای نگهداری و بلند مدت، اصلاً خوب نیست! نقض اصل تک مسئولیتی SOLID و نقطه شروع چاق شدن مدل‌ها همینه: کلاس مدل از پس کارهای زیاد بر میاد، به ساختار دیتابیس گره خورده و بدتر از اون، خیلی از کارها رو با استفاده از magic methodها انجام میده که با دیدن ظاهر کلاس نمیشه متوجه حضورشون شد. نه فقط ما، که IDE هم نمی‌تونه متوجه‌شون بشه؛ در نتیجه تغییر ساده‌ای مثل تغییر نام یه field از مدل، تبدیل به کار دشواری میشه.

ما با شروع استفاده از یه ابزار مثل framework به طور ضمنی پذیرفتیم که در مقابل منفعت‌هایی که برامون داره (مثل سریع‌تر شدن توسعه)، محدودیت‌هایی هم برامون میاره. این‌ها هم محدودیت‌های PHP و Laravel هستن که باید بپذیریمشون و راه چاره‌ای براشون پیدا کنیم. پس بعد از فرستادن لعن و نفرین به این محدودیت‌ها، راه‌حل‌ها رو بررسی می‌کنیم!

سلام بر Query Builder و Collection اختصاصی

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

چاره چیه؟ بعضی‌ها چاره رو استفاده از Repository دیدن: یه کلاس تعریف می‌کنین که متدهایی داره که پرس‌وجو از دیتابیس رو انجام میده و نتیجه رو برمی‌گردونه. طبق تعریف، ریپازیتوری قراره واسطه شما و دیتابیس باشه و جزئیات رو از شما مخفی کنه؛ به همین خاطر به نظر من با حضور Active Record به نحوی که در لاراول پیاده شده، استفاده از ریپازیتوری بی‌معنیه: در نهایت شما با objectهایی از Model سروکار دارین که به دیتابیس گره خورده!

راه بهتر، اینه که این پرس‌وجوها رو به صورت scopeهایی در مدل تعریف کنین:

و بعد استفاده کننده کافیه چیزی که می‌خواد رو درخواست کنه:

این توصیه اگرچه کمکی به لاغر شدن model نمی‌کنه، ولی کمک می‌کنه منطق مدل توی کد پخش نشه و از سخت شدن تغییرات جلوگیری می‌کنه.

برای چاق نشدن مدل در کنار بهره‌مندی از این توصیه، می‌تونیم یه Custom Builder تعریف کنیم:

و بعد به مدل بگیم که از Builder جدیدی که تعریف کردیم استفاده کنه:

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

وقتی نتیجه یه پرس‌وجو یه ردیف از دیتابیس نباشه، لاراول یه Collection از مدل‌ها برمی‌گردونه که خودش امکانات متنوعی به ما میده. برای اینکه بتونیم کارهای بیشتری با داده‌های دریافتی از دیتابیس انجام بدیم، می‌تونیم مجموعه اختصاصی خودمون رو با توسعه (extend) کلاس اصلی کتابخونه تعریف کنیم و بعد به مدل بگیم Custom Collection رو برگردونه.


به رسمیت شناختن مقادیر با Value Object

به کلاس‌هایی معمولی که برابری‌شون براساس مقدارشون سنجیده میشه، نه بر اساس هویت‌شون، می‌گن Value Object. برای مثال، دو تا instance از Money که مقدارشون برابره، با هم برابرن. اغلب می‌تونیم immutable در نظر بگیریمشون: تغییر مقدار ممکن نیست؛ باید یکی دیگه با مقدار جدید ساخت.

وقتی که یه فیلد داریم که منطق خودش رو داره (مثل پول)، وقتی یه تعدادی فیلد همیشه با هم دیده می‌شن، و جایی که بوی Primitive Obsession میاد، می‌تونیم از این روش برای اصلاح کد استفاده کنیم. برای اینکه تبدیل مقادیر به Value Object خودکار انجام بشه، از mutatorها استفاده می‌کنیم:

استفاده از mutatorها برای تبدیل فیلدها به Value Object
استفاده از mutatorها برای تبدیل فیلدها به Value Object

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

استفاده از کلاس‌های کمکی

خیلی وقت‌ها کاری که می‌خوایم انجام بدیم خیلی ارتباطی به مدل نداره، یا با چند مدل مختلف یا سرویس بیرونی سروکار داره. تو این شرایط، می‌تونیم به جای اضافه کردن متد به مدل، مدل رو به یه service class بدیم و ازش بخوایم که به درخواستمون رسیدگی کنه. تو این مسیر، می‌تونیم از الگوهای State و Strategy هم کمک بگیریم و نذاریم خود کلاس سرویس هم خیلی بزرگ و بی‌قواره بشه.

مثل چی؟ مثل احراز هویت کاربر. ممکنه با token انجام بشه، ممکنه با مقایسه password انجام بشه یا روش دیگه‌ای داشته باشه؛ غیر از اطلاعات چند فیلد، بقیه کارها ارتباطی به مدل Uesr نداره و می‌تونه توسط کلاسی مثل Authenticator انجام بشه.

بسته به کاری که می‌خوایم انجام بدیم، مثل آماده‌سازی مدل برای نمایش، ممکنه این کلاس کمکی، شکل‌های دیگه‌ای مثل View و Form و Policy به خودش بگیره. ولی اوّل و آخرش، یه کلاس کمکیه برای جداسازی و تخصصی کردن کارها.

مقاومت در برابر Traitها

برای بیرون کشیدن بخش‌هایی از کلاس، میشه از Trait استفاده کرد. ولی به نظر من استفاده از Traitها به تنهایی خوب نیست؛ چون:

  • همچنان بعضی از پیچیدگی‌های ارث‌بری چندگانه رو داره،
  • با به اشتراک‌گذاری کد، وابستگی به پیاده‌سازی ایجاد می‌کنه،
  • و خوانایی رو پایین میاره: در نهایت این کلاس، همون کلاس بزرگه که فقط تو چند تا فایل محتویاتش رو پخش کردیم (و دنبال کردنش سخت شده!)

اگرچه به کمک interface می‌تونیم تا حدی مشکل مخفی شدن رو جبران کنیم، ولی در خیلی از موارد، استفاده از ترکیب‌بندی به جای ارث‌بری (Composition over inheritance) یا استفاده از الگوی Decorator راه بهتریه.


برملا کردن جادوها

تمام propertyها و methodهایی که دیده نمی‌شن رو با استفاده از @method و @property داکیومنت کنین. اینطوری، IDE می‌تونه استفاده‌هاشون رو شناسایی کنه و توی تغییرات به کمک‌تون بیاد. تو مثال زیر، چند نمونه از کارهای جادویی که توسط لاراول انجام میشه توی داکیومنت‌ها تصریح شده:

استفاده از PHPdoc برای عیان کردن ویژگی‌های مخفی مدل
استفاده از PHPdoc برای عیان کردن ویژگی‌های مخفی مدل


پیشنهاد بعدی برای رمز گشایی از جادوها، استفاده از متد query برای شروع پرس‌وجوست. هر جایی خواستین از متدهای مربوط به پرس‌وجو (مثل where) استفاده کنین، به جای فراخوانی static از متد query کمک بگیرین:

چون در واقع متدهای جادویی این فراخوانی‌های static رو به یه Query Builder پروکسی می‌کنن. با استفاده از متد query و دریافت Query Builder، می‌تونین این خواسته رو صریح (explicit) و مستقیم بیان کنین و IDE هم بهتر می‌تونه توی کدنویسی کمک‌تون کنه.


تو این نوشته، یاد گرفتیم با استفاده از scopeها، builderها و collectionهای اختصاصی، با کمک گرفتن از Value Objectها و service classها، با حسن استفاده از Traitها و در نهایت، با برملاکردن جادوها، می‌تونیم از بدقواره شدن مدل‌های لاراول جلوگیری کنیم.

شما چه روش‌هایی برای Maintainable موندن مدل‌ها می‌شناسین؟