با سلام خدمت دوستان عزیز خودم همونطور که میدونید دانیال هستم و امروز میخوام قوانین SOLID به شکلی قابل درک با مثال های متعدد بهتون یاد بدم ، یادگیری و به کار گیری این قوانین به شدت روی کیفیت کد هایی که میزنید تاثیر داره اینو من با تمام وجود درک کردم هر چی برنامه بزرگ تر میشه میفهمید چقدر مهمه که این اصول توی کد زنی هاتون اجرا کنید .
اگه از اصول SOLID استفاده کنیم کد های با کارایی بالا و مقیاس پذیری بیشتری خواهیم داشت که میتونیم اونا در جاها و پروژه های مختلف استفاده کنیم علاوه بر این موارد خیلی راحت میتونیم کدهامون تست کنیم و توسعه سریع تری هم خواهیم داشت .
از زمانی که برنامه نویسی شی گرا جایگزین برنامه نویسی فانکشنال شد برنامه نویسان زیادی به سمت شی گرایی رفتند و چون شی گرایی تازه اول راه بود هر کسی یه جوری کد میزد و چهارچوب و قوانینی خاصی نبود ، حالا این عدم وجود چهارچوب و قوانین روشن و واضح باعث شد خیلی از این برنامه نویس ها توی توسعه برنامه هاشون ، خوانایی کدهاشون ، ساختار کد نویسشون دچار مشکلاتی بشن .
با بیشتر شدن این مشکلات برنامه نویس ها به فکر یه سری قوانین افتادن که با رعایت کردنشون مشکلاتی از این دست نداشته باشند و اینطوری شد که اصول SOLID و همچنین دیزاین پترن ها بع دنیای برنامه نویسی شی گرا معرفی شدند .
ببین دوست من فرض کن رفتی یه فروشگاه مبلمان و میخوای برای خونتون مبل بخری ، وقتی وارد میشی یه آقایی میاد شما راهنمایی میکنه که بتونید مبل مورد نظرتون انتخاب کنید ، حالا فرض کنید از یه مبلی خوشتون اومد و خواستید بخریدش در این حالت شما میرید صندوق و هزینه پرداخت میکنید دیگه اون آقایی فروشنده که بهتون کمک کرد مبلتون انتخاب کنید نمیاد از شما پول بگیره ، شما رو به صندوق راهنمایی میکنه .
خب حالا مبل خریدید و میخوایید بیاریدش خونه ، دیگه اون آقای صندوق دار و فروشنده که مبل براتون نمیارند خونه وظیفه این کار با باربری هستش . پس همونطور که دید توی هر کاری هر کسی یه وظیفه ای داره اینطوری کارا خیلی با کیفیت تر پیش میره ، خطایابی راحت تره و اگه قرار باشه تغییری ایجاد بشه فقط توی قسمتی که مشکل داره تغییر ایجاد میشه نه همه قسمت ها .
اصل اول قوانین سالید (Single Responsibility) میگه هر کلاسی فقط باید یه کار انجام بده و فقط باید به یه دلیل تغییر کنه
بیایید مثال بالا با هم به شکل برنامه نویسی در بیاریم :
class BuyingFernicher { public function selection() { //اتتخاب مبل دلخواه به کمک آقای فروشنده } public function buying () { // پرداخت هزینه به صندوق دار } public function shipping() { // ارسال کالا به آدرس } }
خب به نظر شما الان اصل اول قوانین Solid اینجا اجرا شده ؟ آیا اینجا کلاس BuyingFernicher فقط داره یه کار انجام میده ؟ خب مسلما جواب منفی .
الان کلاس BuyingFernicher داره سه کار انجام میده ینی شما مثلا اگه بخوایین تغییری در شیوه ارسال به وجود بیارید باید باز بیایید توی این کلاس یا اگه بخوایین تغییری توی نحوه محاسبه قیمت به وجود بیارید باید بیاید توی این کلاس باز و این اشکال اصل اول SOLID را که میگه هر کلاسی فقط یه کار باید انجام بده و فقط به یه دلیل باید تغییر پیدا کنه رد میکنه .
حالتی که اصل Single Responsibility رعایت کرده باشیم :
class selection{} class buying{} class shipping{}
خب الان هر بخشی از کار به یه کلاس جدا تبدیل شده و اگه ما بخواییم نحوه باربری را تغییر بدیم یا ویژگی و امکانی بهش اضافه کنیم میریم توی کلاس shipping اگه بخواییم یه شیوه پرداخت جدید اضافه کنیم و مثلا پرداخت با بیت کوین هم داشته باشیم میریم توی کلاس Buying .
پس طبق قانون اول اصل SOLID یه کلاس فقط باید یه کار انجام بده و به یه دلیل هم تغییر کنه مثل مثال بالا که الان کلاس Shipping فقط داره کار ارسال انجام میده یا کلاس selection فقط داره کار کمک به انتخاب کاربر انجام میده .
چرا باید از این اصل استفاده کنیم همیشه ؟
یه نکته دوستانه اگه بخوام بتون بگم اینه که کلا این اصل خیلی جدی بگیرید و حتی توی نوشتن توابعتون هم این اصل رعایت کنید ینی هر تابع فقط یه کار انجام بده .
این اصل میگه کد هاتون باید جوری بنویسید که برای توسعه باز باشند و برای تغییر بسته باشند
حالا این مفهوم ینی چی اصلا ؟ اصلا باز و بسته بودن ینی چی اینجا ؟ ببین منظور اصلی این حرف اینه که اگه من یه روزی بخوام یه ویژگی جدیدی به کلاسم اضافه کنم بتونم خیلی راحت و بدون دستکاری سورس کد اصلی این کار انجام بدم .
خب از اونجایی که این اصل باید با مثال جا بیفته به مثال پایین توجه کنید :
فرض کنید یه شرکتی به شما پیام میده که من میخوام یه وبسرویس برام بنویسی که هر پیامی خواستم بتونم SMS کنم خب تیکه کد شما مثل پایین میشه احتمالا :
class Message { public $phone; public $content; public function __construct($phone,$content) { $this->phone = $phone; $this->content= $content; } } class SendMessage { public function Send($Messages = [] ) { return 'Message : '. $Messaeg; } } $Message1 = new Message('Hello World!','09169987453'); $Message2 = new Message('Hello World Is Good !','09148987453'); $SendMessage = new SendMessage(); $SendMessage->Send([$Message1,$Message2]);
حالا بعد از یه ماه شرکته دوباره زنگ میزنه میگه آقا من میخوام بتونم از طریق ایمیل هم پیام بفرستم و شما باید وب سرویس ایمیل هم به من تحویل بدی خب الان باید کدت جوری ویرایش کنی که بتونه هم ایمیل بفرسته و هم اس ام اس :
class SmsMessage { public $phone; public $content; public function __construct($phone,$content) { $this->phone = $phone; $this->content= $content; } } class EmailMessage { public $email; public $content; public function __construct($email,$content) { $this->email = $email; $this->content= $content; } } class SendMessage { public function Send($Messages = []) { foreach ($Messages as $message) { if ($message instanceof SmsMessage) SmsSendingFunc($message->content, $message->phone); elseif ($message instanceof EmailMessage) EmailSendingFunc($message->content, $message->email); } } } $SmsMessage = new SmsMessage('Hello World !','09169987453'); $EmailMessage = new EmailMessage('Hello World!','info@gmail.com'); $SendMessage = new SendMessage(); $SendMessage->Send([$SmsMessage, $EmailMessage]);
خب همونطور که دیدم ما برای اینکه بیاییم مشخص کنیم نوع پیامی که داریم ارسال میکنیم چیه تا بتونیم پیاممون با تنظیمات اختصاصی اون نوع ارسال کنیم اومدیم و از instansOf استفاده کردیم ینی در واقع اومدیم توی خود سورس کدمون و اون تابع اصلی تغییر دادیم حالا اگه پنجاه روش ارسال پیام دیگه مشتری ما خواست اضافه کنه باید به همین ترتیب 50 بار دیگه اینکار بکنیم و این کار خوبی نیست و یه برنامه نویس حرفه ای که اصول SOLID بلده میدونه که این کار بر خلاف قانون دوم SOLID هستش .
توی قانون دوم SOLID به وضوح میگه آقاجون کلاستو یه جور بنویس که اگه بعدا خواستی تغییرش بدی مثل کد بالا ، نیایی دست ببری توی خود سورس کد اصلی . سورس کد همیشه باید برای تغییر بسته باشه ینی اگه قرار بر تغییری باشه توی سورس کد نباشه
راه حل چیه حالا ؟
خوب ما باید کلاسمون جوری بنویسیم که واسه هر تغییری نیاییم توی سورس کد اصلی و مثل بالا کدمون تغییر بدیم همچنین هر تغییری خواستیم بدیم (مثلا اگه خواستیم پیام به یه روش دیگه ارسال کنیم ) بتونیم خیلی راحت و آسون بدون دست بردن توی سورس کد این کار انجام بدیم برای اینکه به اهداف بالا برسیم باید و باید از قابلیت interface استفاده کنیم .
حالا این دوست عزیزمون ینی آقای interface چیکار میکنه برامون ؟؟
ببین دوست من ، ما برای این که نیاییم هر بار از instansOf استفاده کنیم و کد اصلی تغییر بدیم باید یه تابعی مشترک (sendFunc)داشته باشیم که توی همه کلاس هامون هم باشه درسته این تابع بین همه کلاس ها مشترکه ولی بر اساس نوع آبجکت که از چه کلاسی هستش کد های متفاوتی اجرا میکنه و به این ترتیب دیگه نیازی نیست توی سورس کد اصلی دست برد .
مثلا میاد میبینه که message$ یه نمونه از کلاس SmsMessage هستش پس میاد توی کلاس SMSMessage و از اونجا تابع sendFunc اجرا میکنه . حالا کدهایی که توی این کلاس نوشتیم با کد هایی که توی کلاس EmailMessage نوشتیم متفاوته این مکانیزم ما با استفاده از interface ها پیاده سازی میکنیم .
interface MessageSend{ public function sendFunc(); } //SMSکلاس ارسال اس ام اس که شماره تلفن توی ورودی میگیره class SmsMessage implements MessageSend { public $phone; public $content; public function __construct($phone,$content) { $this->phone = $phone; $this->content= $content; } public function sendFunc() { SmsSendingFunc($this->content, $this->phone); } } //EMAIL کلاس ارسال ایمیل که آدرس ایمیل توی ورودی میگیره class EmailMessage implements MessageSend { public $email; public $content; public function __construct($email,$content) { $this->email = $email; $this->content= $content; } public function sendFunc() { EmailSendingFunc($this->content, $this->email); } } //Send کلاس ارسال پیام ها که با توجه به نوع پیام به آدرس مورد نظر ارسال میکنه class SendMessage { public function Send($Messages = []) { foreach ($Messages as $message) { $message->sendFunc(); } } } $SmsMessage = new SmsMessage('Hello World !','09169987453'); $EmailMessage = new EmailMessage('Hello World!','info@gmail.com'); $SendMessage = new SendMessage(); $SendMessage->Send([$SmsMessage, $EmailMessage]);
در اپتدا یه interface میسازیم و اسمش میزاریم MessageSend حالا یه تابع توش میزاریم به اسم SendFunc این همون تابع مشترک ما هستش .
در گام بعدی باید با implements کردن این اینترفیس متد sendFunc به همه کلاس هامون اضافه کنیم با اضافه کردن این متد لاراول میتونه متوجه بشه که اگه مثلا نوع اون message$ از کلاس SmsMessage بود بیاد و محتویات داخل این تابع اجرا کنه .
در گام آخر توی قسمت سورس کد اصلیمون به جای استفاده از instanseOf میاییم از همون تابع sendFunc() استفاده میکنیم حالا لاراول بر اساس نوع کلاس message$ میتونه متوجه بشه که کدوم متد اجرا کنه .
حالا هر متد دیگه ای که قرار باشه اضافه بشه کافیه بیاییم کلاسش درست کنیم و بعد از اینترفیسمون implement کنیم به این ترتیب یه تابع sendFunc() درست میشه توی کلاس جدیدمون و حالا بر اساس تنظیمات دلخواه پیام به شیوه های دیگه میفرستیم مثلا با دود !
این اصل یه اصل خیلی ساده هستش ن شما فرض کن کلاس A داری و حالا بعد یه مدتی میخوایی این کلاس گسترش بدی و یه سری ویژگی ها اضافه کنی بهش ، پس یه کلاس دیگه میسازی به اسم B و اونو از کلاس A اکستند (extend) میکنی حالا اینجا این اصل وارد میشه و میگه :
آقا چون الان کلاس B یه زیر کلاس از کلاس A هستش و همه ویژگی های کلاس A داره پس باید بتونیم هر جایی که شی از کلاس A ساختیم با کلاس B بسازیم مثلا مثل تیکه کد زیر :
توی کد زیر ما از اول فقط یه کلاس A داشتیم بعدا تصمیم گرفتیم برنامه توسعه بدیم پس یه کلاس B هم ساختیم و اون از کلاس A اکستند کردیم و ویژگی هایی جدیدی که میخواستیم اضافه کردیم بهش . خب طبق قانون Liskov الان باید بدون هیچ مشکلی با هر نمونه ای که از کلاس B ساختیم به کل متد های کلاس A دسترسی داشته باشیم . حالا بیاییم با هم بررسی کنیم ببینیم آیا این اصل اینجا رعایت شده یا نه !
class A { public function methodA() { return 'I am Method A!'; } public function methodB() { return 'I am Method B!'; } public function methodC() { return 'I am Method C !'; } } class B extends A { public function newFeature1() { return 'I am New Feature1'; } public function newFeature2() { return 'I am New Feature2'; } public function methodC() { return 'I am Method C From Class B !'; } } //$object = new A; $object = new B;
خب ما اول object از کلاس A ساخته بودیم ولی چون کلاس B هه ویژگی های کلاس A داره و درواقع ازش ارث بری کرده پس باید بتونیم بدون هیچ مشکلی object به جای اینکه از کلاس A بسازیم بیاییم از کلاس B بسازیم تا بتونیم علاوه بر همه متد هایی که در کلاس A هستند به همه متد های جدید هم دسترسی داشته باشیم و از قابلیت های جدید هم بتونیم استفاده کنیم .
حالا اگر برنامه ما اصل Liskov رعایت کرده باشه به راحتی این کار میتونیم انجام بدیم ولی توی این کدی که ما نوشتیم این اصل رعایت نشده چون ما الان اگر از کلاس B یه شی جدید بسازیم و بخواییم به methodC دسترسی داشته باشیم . برنامه میاد methodC خود کلاس B اجرا میکنه و ما نمیتونیم اون methodC که توی کلاس A هست با ساخت نمونه از کلاس B اجرا کنیم .
$object = new B; $object->methodC() ; // I am Method C From Class B !
اگه اینترفیس X چندتا متد داشته باشه هیچ کلاسی نباید باشه که از اون اینترفیس (ینی همون X) impalements شده باشه و یکی از متد های اینترفیس X نیاز نداشته باشه .
فرض کن یه شرکت برنامه نیوسی میاد میگه سلام من شنیدم شما برنامه نویس خیلی خوبی هستید ، من چون کارمندام خیلی زیادن میخوام یه برنامه ای بنویسی که کارمندامو بتونم مدیریت کنم . کارمندای شرکت من یا برنامه نویس هستند یا کد ها تست میکنند (Tester) البته کارمندهای برنامه نویس میتونند کار تست هم انجام بدن . کد زیر ببین بهتر متوجه بشی :
class Programmer { public function coding() { return 'Coding . . . '; } } class Tester { public function testing() { return 'Testing . . . '; } } class ProjectManagment { public function process($user) { if ($user instanceof Programmer) $user->coding(); elseif ($user instanceof Tester) $user->testing(); } } $projectManagment = new ProjectManagment(); $projectManagment->process($user);
خب برای کد نویسی پروژه ای که به ما داده شده ما دوتا کلاس داریم یکی کلاس بچه های کد نویس (Programmer) یکی کلاس بچه های تستر (Testers) حالا توی هر کدوم از این کلاس ها فانکشن هایی وجود داره که در واقعکار کد نویسی و یا کار تست پروژه انجام میده .
یه کلاس دیگه هم داریم به اسم ProjectManager که همونطور که ازز اسمش پیداست کار مدیریت پروژه انجام میده این کلاس یه تابع داره به اسم process که یه مقدار ورودی میگیره و مشخص میکنه که اون کاربره باید چی کار انجام بده .
ولی کد بالا یه مشکلی داره ، اونم اینه که اصل دوم SOLID ینی اصل Open/Close رعایت نکرده . کجا رعایت نشده این اصل ؟؟ همون جایی که اومدیم از instanceOf استفاده کردیم ، ما با استفاده از instanceOf در واقع اومدیم کد اصلی دستکاری کردیم .
برای این که این مشکل حل کنیم باید بیاییم از interface ها استفاده کنیم ولی باید خیلی دقت کنیم که اینجا کلاس برنامه نویس هم میتونه کار تست انجام بده و هم کار کد زنی پس اینترفیس ما دوتا تابع داره یکی تابع code یکی دیگه تابع test :
interface processJob { public function code(); public function test(); }
بعد از اینکه اومدیم اینترفیس مربوطه ساختیم طبیعتا باید کلاس ها را ازش impalements کنیم و چون هر دو متد توی یه دونه interface قرار دادیم با impalements کردن اون interface هر دوتابعی که نوشتیم میاره توی اون کلاس مربوطه . حالا همونطور که گفتیم یه برنامه نویس میتونه هم کار کد زنی انجام بده و هم کار تست پس هر دو تابعی که توی اینترفیس ها معرفی شد توی کلاس Programmer استفاده شد و هیچ کدومشون بیکار نموند .
class Programmer implements processJob { public function code() { return 'Coding . . .'; } public function test() { return 'Testing . . .'; } }
ولی هر کسی که تست انجام میده نمیتونه کد زنی کنه پس همونطور که میبینید توی کلاس Tester الان متد code داره مثدار Null برمیگردونه ، به عبارتی یکی از متد هایی که توی اینترفیس تعریف کردیم بیکار موند و این بر خلاف اصل چهارم قوانین SOLID است .
class Tester implements processJob { public function code() { return null; } public function test() { return 'Testing . . .'; } }
خب حالا را حل چیه ؟
ما باید بیاییم interface هامون جدا کنیم اینجوری مثلا اگه کلاس tester به متد code نیازی نداشت میاد فقط از اینترفیس testInterFace استفاده میکنه و دیگه اون حالتی که حتما باید دوتا متد یه اینترفیس وجود داشته باشه از بین میره .
interface codeInterFace { public function code(); } interface testInterFace { public function test(); }
حالا این اینترفیس ها به شکل پایین impelements میشن :
class Programmer implements codeInterFace,testInterFace { public function code() { return 'Coding . . .'; } public function test() { return 'Testing . . .'; } } class Tester implements testInterFace { public function test() { return 'Testing . . .'; } }
خب اینم از قانون چهارم حالا بریم سراغ آخرین قانون .
کلاس های سطح بالا نباید به کلاس های سطح پایین وابسته باشند
این اصل خیلی شبیه به اصل open/Close هستش حالا میاییم با مثال این اصل کامل براتون توضیح میدیم .هر سایتی یه سیستم اطلاع رسانی داره ، مثلا اگه کاربری ثبت شد یا خریدی انجام گرفت یا هر چیز دیگه با استفاده از این سیستم به مدیر اطلاع رسانی میشه . حالا نکته کار اینجاست که این اطلاع رسانی میتونه با ایمیل یا پیامک یا هر چیز دیگه ای باشه ولی وقتی داریم این سیستم پیاده سازی میکنیم به یه مشکل رایج برمیخوریم کد زیر ببینید و اون مشکل سعی کنید خودتون حدس بزنید :
<?php class Mailer { public function send($message) { return $message; } } class SendMessage { private $mailer; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function sendComplateMessage() { $message = "Hello" $this->mailer->send($message); } }
#کد چی میگه ؟
ببین یه کلاس Mailer داریم که برامون کار ارسال ایمیل انجام میده یه کلاس SendMessage داریم که پیام مربوطه میسازه و با استفاده از کلاس Mailer اونو ارسال میکنه .
#کلاس سطح بالا و سطح پایین چیه ؟
خب اول یه توضیح کوچیکی بدم راجب کلاس سطح بالا و کلاس سطح پایین . ببین دوست من کلاس سطح کلاسی واسه انجام درست وظایفش به یه سری کلاس های دیگه نیاز داره ، مثلا توی مثال بالا کلاس سطح بالا میشه همون کلاس SendMessage چون برای اینکه بتونه پیامی ارسال کنه به کلاس Mailer احتیاج داره .
#حالا مشکل کجاست ؟
الان مشکل دقیقا اینجاست که کلاس سطح بالای ما ینی کلاس SendMessage برای اینکه بتونه کارش درست انجام بده به کلاس Mailer احتیاج داره و فردا روزی اگه قرار شد این ارسال پیام با SMS انجام بشه باید کلاس SendMessage تغییر بدیم .
#راه حل چیه حالا ؟
کلا مشکلات مربوط به وابستگی معمولا با اینترفیس ها حل میشه الانم باید از اینترفیس ها استفاده کنیم و این مشکل حل کنیم ، باید کاری بکنیم که کلاس SendMessage هیچ وابستگی به کلاس Mailer نداشته باشه تا هر وقت که خواستیم به جز ایمیل مثلا SMS هم ارسال کنیم دیگه نیاییم خود کلاس تغییر بدیم .
<?php interface Sender { public function send($message); } class Mailer implements Sender { public function send($message) { // TODO: Implement send() method. } } class SMSer implements Sender { public function send($message) { // TODO: Implement send() method. } } class SendMessage { private $sender; public function __construct(Sender $sender) { $this->sender = $sender; } public function sendComplateMessage() { $message = "Hello" $this->sender->send($message); } } $send = new SendMessage(new SMSer());
توی این تیکه کد ما اومدیم از interface ها استفاده کردیم ، اینجا ما اومدیم یه interface ساختیم به اسم Sender یعدش این اینترفیس implements کردیم حالا اومدیم توی کلاس SendMessage وابستگی ها از بین بردیم و به جای کلاس های Mailer و Smser از اینترفیس Sender استفاده کردیم و به این ترتیب دیگه فرقی نمیکنه که ورودی ما از چه نوعی باشه میخواد از کلاس SMSer باشه یا Mailer یا هر کلاس دیگه ای هیچ فرقی نمیکنه .
خب دوستان بلاخره این مقاله طولانی هم به اتمام رسید امیدوارم تونسته باشم مفاهیم SOLID خیلی خوب بهتون توضیح داده باشم از همتون ممنونم که تا اینجا منو همراهی کردید اگه سوالی داشتید حتما حتما بپرسید و همچنین اگه دوس داشته باشید میتونید منو توی لینکدین هم دنبال کنید با تشکر از لطف شما .
دانیال صناعی