در این آموزش میخواهیم در مورد اصل Single Responsibility در لاراول صحبت کنیم. همانطور که قبلا در مورد اصول پنجگانه SOLID صحبت کردیم، این اصل، اولین مورد است. اگر به منابع مختلف در توصیف اصل Single Responsibility نگاه کنید، تفاوتهایی در تعریف آن خواهید دید. با این حال، به زبان ساده و خلاصه، مفهومش چنین چیزی است:در کد شما، هر کلاس باید نقش بسیار خاصی داشته باشد. یعنی نباید مسئول بیش از یک هدف باشد. و هر زمان که تغییری در آن کلاس مورد نیاز باشد، به شرطی تغییرات در کلاس اعمال شود که آن یک مسئولیت خاص، تغییر کرده باشد. شاید ساده به نظر برسد اما این اصل پایهی اساسی برای نظم دادن به کدهای یک پروژه است. بعدا خواهید فهمید کدی که با این تفکر نوشته شود خیلی سادهتر قابل تست خواهد بود و مشکلات و باگهای بسیار کمتری تولید میکند.
بگذارید این اصل را با یک مثال کاربردی بررسی کنیم. بر خلاف سایر منابع موجود در وب که مثالهایی ساده و تکراری را بررسی میکنند که در نگاه اول، شما آنها را به سرعت درک می کنید و وقتی میخواهید همان مفاهیم را در دنیای واقعی و در پروژهی خود پیاده کنید، به بن بست میرسید، بیایید به مثالی خاص و کاربردی بپردازیم. سبکی از کدنویسی که بارها و بارها در فریم ورک لاراول دیدهایم و شاید برای نوشتن پروژههای خود از آن استفاده کنیم.
هنگامی که یک برنامهی لاراولی ما یک درخواست وب دریافت میکند، URL با مسیرهایی که در web.php و api.php تعریف کردهاید مطابقت داده میشود و اگر مطابقت داشت، دادههای درخواست به Controller مربوطه میرسند. در اینجا یک Controller معمولی در برنامه های واقعی به نمایش گذاشته شده است:
class UserController extends Controller {
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'first_name' => 'required',
'last_name' => 'required',
'email' => 'required|email|unique:users',
'phone' => 'nullable'
]);
if ($validator->fails()) {
Session::flash('error', $validator->messages()->first());
return redirect()->back()->withInput();
}
// create new user
$user = User::create([
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'email' => $request->email,
'phone' => $request->phone,
]);
return redirect()->route('login');
}
}
همه ما اینگونه کد نوشته ایم و به راحتی می توانیم بفهمیم که چه کاری انجام می دهد: ثبت کاربران جدید. به نظر می رسد خوب است و خوب کار می کند، اما یک مشکل اساسی وجود دارد. آیندهنگر نیست! منظور من از آیندهنگری این است که بعدها، این بخش از کد بدون ایجاد آشفتگی، پذیرای تغییرات نخواهد بود. ولی چرا؟
می توانید بگویید که این تابع برای مسیرهایی است که در فایل web.php تعریف شده است. یعنی صفحات سنتی که توسط سرور رندر میشوند و نتیجه در قالب یک صفحه HTML به کاربر ارسال میشود. خب با این حال، چند روزی میگذرد و اکنون مشتری یا کارفرمای شما در حال توسعه یک برنامه تلفن همراه است. به این معنی که این کد برای کاربرانی که از دستگاههای تلفن همراه ثبت نام میکنند، فایدهای نخواهد داشت. حالا در این وضعیت چه کار میکنی؟ یک route مشابه در فایل api.php ایجاد میکنی و یک کنترلر جدید با متدی مبتنی بر JSON برای آن مینویسی؟ و بعد چه؟ تمام کدهای قبلی را کپی میکنی، چند تغییر ایجاد میکنی و تصور میکنی کار را به درستی انجام دادهای؟
این در واقع کاری است که بسیاری از توسعه دهندگان انجام می دهند و خود را برای شکست در آینده آماده می کنند.
مشکل کار اینجاست که HTML و JSON تنها فرمتهای API در جهان نیستند (فقط برای استدلال، صفحات HTML را یک گونه از API در نظر گرفتیم). در مورد وضعیتی که مشتری دارای یک سیستم قدیمی است که با فرمت XML اجرا می شود، چطور؟ و سپس یکی دیگر برای SOAP وجود دارد. و gRPC. و خدا می داند این فرمتها در آینده چقدر متنوعتر خواهند شد.
ممکن است همچنان به فکر ایجاد یک فایل جداگانه برای هر یک از این انواع API باشید و کد موجود را کپی کنید و کمی آن را تغییر دهید. ممکن است ندایی درونی به شما هشدار دهد که این روش عاقلانه نیست و شما در ذهنتان برای تغییر رویکرد، فکر کنید. یا شاید با همکارتان در این باره بحث کنید، اما وقتی لحظهنگر باشید، به خودتان میگویید همه چیز که خوب کار می کند، پس چرا شکایت کنیم؟
اما بعد از این ماجرا و در آینده، روزی فراخواهد رسید که ضربهی روحی اصلی را تجربه خواهید کرد. تغییر، که دشمن اصلی توسعهی نرمافزار است به سراغتان میآید. در چنین اوضاعی، فرض کنید نیازهای مشتری یا کارفرمای شما تغییر کرده است. آنها اکنون می خواهند که در زمان ثبت نام کاربر، آدرس IP دریافت شود و همچنین گزینه ای برای پذیرش شرایط و ضوابط در نظر گرفته شود.
اوه! حالا ما تعداد زیادی فایل داریم که باید برای اعمال این تغییر، ویرایش شوند و باید مطمئن شویم که منطق در همه آنها دقیقاً یکسان است. حتی یک خطا می تواند باعث زیان بزرگ در کسب و کار شود. و اکنون وحشت را در برنامه های SaaS که در مقیاس بسیار بزرگتری هستند تصور کنید، تازه میفهمید که پیچیدگی کد زیاد است و در این وضعیت تازه به دنبال راهکار میگردید. خدا کمکتان کند!
البته امید خود را از دست ندهید. یک جملهی معروف هست که میگوید همیشه نمیتوان همه چیز را پیشبینی کرد، اما همیشه میتوان برای هر اتفاقی آماده بود. شاید خیلی شعار گونه باشد، اما همین رویکرد باعث پایهگذاری خیلی از قواعد ارزشمند برای کدنویسی آیندهنگر شد. حال برای حل مشکل بیایید با هم بررسی کنیم که چگونه به این وضعیت جهنمی رسیدیم؟
پاسخ این است که متد store در UserController که بالاتر بررسی کردیم و به ظاهر بسیار بیضرر به نظر میرسد، چندین کار مختلف را انجام میدهد: درخواست ورودی را تأیید میکند، redirect ها را مدیریت میکند و کاربران جدیدی ایجاد میکند. کارهای زیادی انجام می دهد! و بله، همانطور که ممکن است متوجه شده باشید، دانستن نحوهی ایجاد کاربران جدید در سیستم(اعتبارسنجی اطلاعات ورودی، تولید خطا در صورت وجود مشکل در اطلاعات، ارتباط با پایگاه داده و ثبت اطلاعات) در واقع نباید وظیفهی یک Controller باشد. یک کنترلر فقط باید هدایت کننده باشد نه چیز دیگری! یعنی یک کنترلر نهایتا میتواند اطلاعات ورودی را به کلاس دیگری برای ایجاد کاربر جدید و پس از آن برگرداندن جواب به کاربر، مورد استفاده قرار گیرد.
اگر بخواهیم نحوهی ایجاد کاربر را از متد store خارج کنیم و آن را در یک کلاس جداگانه قرار دهیم، اکنون دو کلاس خواهیم داشت که هر یک مسئولیت واحدی برای رسیدگی دارند. در حالی که این کلاسها میتوانند با فراخوانی متدهای خود، از یکدیگر کمک بگیرند، اما اجازه ندارند بدانند در داخلشان چه میگذرد. به نسخه بهبود یافتهی UserController نگاه کنید:
class UserController extends Controller {
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'first_name' => 'required',
'last_name' => 'required',
'email' => 'required|email|unique:users',
'phone' => 'nullable'
]);
if ($validator->fails()) {
Session::flash('error', $validator->messages()->first());
return redirect()->back()->withInput();
}
UserService::createNewUser($request->all());
return redirect()->route('login');
}
}
اکنون به کد بالا نگاه کنید: بسیار فشرده تر و قابل درک است و مهمتر از همه، سازگار با تغییر است. در ادامه بحث قبلی که در آن به چندین نوع مختلف API با کارکرد یکسان ولی در محیطهای مختلف رسیدیم، هر کدام از آنها اکنون یک تابع مشترک را فراخوانی میکنند و آن UserService::createNewUser($request->all()) است. دیگر، Controller ها در نحوهی ایجاد کاربر دخیل نیستند و چیزی از این فرآیند نمیدانند. اگر تغییراتی در منطق ثبت نام کاربر نیاز باشد، کلاس UserService مسئول پاسخگویی به آن تغییر خواهد بود. در حالی که Controller ها اصلاً نیازی به تغییر ندارند. مثلا اگر بعد از ثبت نام کاربر نیاز به ارسال پیامک باشد، باز هم UserService مسئول آن است. (با فراخوانی متدی از کلاس دیگر که وظیفهاش ارسال پیامک است) و دوباره Controller ها دست نخورده باقی می مانند.
با اینکه کد بهینه شدهی UserController باز هم امکان بهینهسازی دارد، اما به نظر میرسد مثالی که دنبال کردیم برای درک اصل Single Responsibility در لاراول، مناسب و کافی باشد. برای تمرین بیشتر، سعی کنید در مثال بالا، validate کردن درخواست را از کنترلر خارج کنید و در جایی مناسب قرار دهید( Form Request).