SOLID چیست ؟
سلام به همگی در این مقاله به بررسی SOLID چیست ؟ می پردازیم اصول طراحی شی گرا SOLID چیست؟ solid در واقع 5 اصل اولیه طراحی شی گرا است و خود واژه سالید (solid) در اصطلاح first five object-oriented design یا ODD است و توسط Robert C. Martin یا شاید به نام Uncle Bob شناحته می شود اولین بار که نامش رو شنیدم مثل شما خندیدم ? اسمش برام جالب بودش در ادامه بیشتر در مورد پنج اصل طراحی شی گرا (SOLID) صحبت خواهیم کرد.
در این مقاله چه چیزی را یاد می گیرید ؟
- solid چیست ؟
- نحوه استفاده از solid در برنامه نویسی
- کاربردهای solid
چرا باید از 5 اصل Solid استفاده کنیم ؟
این اصول ، هنگامی که در کنار هم قرار گرفته اند ، برای یک برنامه نویس امکان ایجاد نرم افزاری را فراهم می کند که نگهداری و گسترش آن آسان باشد. علاوه بر موارد گفته شد این پنج اصل از code smells جلوگیری و refactor کردن کد را برای توسعه دهنده راحت می کند این اصول از مدیریت پروژه به صورت agile پیروی می کند.
یه توضیخ کوچیک برای آن دسته از دوستانی که نمی دانند agile چیست ؟ در ادامه توضیح می دهیم.
Agile چیست ؟
مدیریت پروژه با رویکرد اجایل (چابک) یک رویکرد در مدیریت پروژه است که از پاسخگویی دائم به تغییرات به جای پیروی از یک برنامه ریزی دقیق و از پیش تعیین شده حمایت می کند. مدیریت چابک (اجایل) یک روش شناسی نیست. بلکه مجموعه ای از اصول است (که به اسم بیانیه مدیریت چابک شناخته می-شود) و حاکی از آن است که ما باید با چه رویکردی به مدیریت پروژه بپردازیم.
همانطور که گفتیم Solid شامل 5 اصل است که در پایین می توانید این اصول را مشاهده کنید.
- S – Single-responsiblity principle
- O – Open-closed principle
- L – Liskov substitution principle
- I – Interface segregation principle
- D – Dependency Inversion Principle
به نظر کمی راحت تر شد زمانی که گفته می شود SOLID شاید به نظر یکم پیچیده برسد ولی اینطور نیست در پایین هرکدام از این اصل توضیح داده شده است.
Single-responsibility Principle یا S.R.P
کلاس فقط و تنها باید به یک دلیل تغییر کند یعنی کلاس باید یک کار برای انجام دادن داشته باشد.
به عنوان مثال ما یکسری شئی داریم و می خواهیم جمع تمامی نحوای شکل ها را داشته باشیم برای اینکار باید کلاسی مثل زیر درست کنیم.
1 2 3 4 5 6 7 8 9 10 11 12 | class Circle { public $radius; public function construct($radius) { $this->radius = $radius; } } class Square { public $length; public function construct($length) { $this->length = $length; } } |
در بالا در واقع دو کلاس ایجاد شده که هرکدام مربع و دایره های ما را پیاده سازی می کنند.
حل برای اینکه عمل محسابه این دو شئی را محاسبه کنیم لازم است تا یک کلاس به نام AreaCalculator ایجاد کنیم مثل زیر
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class AreaCalculator { protected $shapes; public function __construct($shapes = array()) { $this->shapes = $shapes; } public function sum() { // logic to sum the areas } public function output() { return implode('', array( "", "Sum of the areas of provided shapes: ", $this->sum(), "" )); } } |
و در کلاس بالا عمل محسابه انچام می شود و نحوه استفاده از کلاس های بالا نیز همانند زیر خواه بود.
1 2 3 4 5 6 7 | $shapes = array( new Circle(2), new Square(5), new Square(6) ); $areas = new AreaCalculator($shapes); echo $areas->output(); |
همانطور که دیدید هر کلاس در آن واحد یک عمل را انجام می داد پس تا اینجه اصل شماره یک SRP را رعایت کردیم.
Open-closed Principle
شئی ها یا Entity باید برای گسترش باز باشند اما برای اصلاح بسته باشند و این به این معنی است که کلاس باید برای گسترش بدون تغییر کلاس امکان پذیر باشد.
کد زیر را در نظر بگیرید تا بیشتر توضیح بدیم.
1 2 3 4 5 6 7 8 9 10 | public function sum() { foreach($this->shapes as $shape) { if(is_a($shape, 'Square')) { $area[] = pow($shape->length, 2); } else if(is_a($shape, 'Circle')) { $area[] = pi() * pow($shape->radius, 2); } } return array_sum($area); } |
کد بالا برای جمع ناحیه های دایره و مربع است در نظر بگیرید بخواهیم شکل های مختلف دیگری نیز به آن اضافه کنیم در آن صورت باید if و else های بیشتری به آن اضافه کنیم ولی انجام همچین کاری مخالف اصل دوم ما یا Open-closed Principle است پس باید چه کنیم ؟
هر کلاس باید ویژگی های خود را داشته باشد یعنی ما باید عمل مخاسبه مربوط به هر شئی را در کلاس مربوط به آن تعریف کنیم.
برای همین منظور کلاس Square و Circle همانند زیر تغییر خواهد کرد.
1 2 3 4 5 6 7 8 9 | class Square { public $length; public function __construct($length) { $this->length = $length; } public function area() { return pow($this->length, 2); } } |
در بالا متد area به منظور محاسبه محیط مربع است. کلاس Circle نیز باید تغییر کند.
در نهایت متد sum که منظور جمع محیط های مختلف بود باید به شکل زیر تغییر کند.
1 2 3 4 5 6 | public function sum() { foreach($this->shapes as $shape) { $area[] = $shape->area(); } return array_sum($area); } |
همانطور که دید در واقع متد هر کلاس که گرفته می شود شامل area است و بعدا آنها با هم محاسبه می شود.
بازم یک مشکلی در اینجا پیش میادش چه طور مطمئن باشیم که آن کلاسی که به sum پاس داده می شود حتما دارای متد area است ؟
برای اینکه همچین چیزی را بررسی کنیم کافی است یک Interface بسازیم
پس کد مربوط به کلاس Circle باید مثل زیر تغییر کند.
1 2 3 4 5 6 7 8 9 10 11 12 | interface ShapeInterface { public function area(); } class Circle implements ShapeInterface { public $radius; public function __construct($radius) { $this->radius = $radius; } public function area() { return pi() * pow($this->radius, 2); } } |
با این کار ما برنامه نویس را مجبور کردیم تا زمانی که یک کلاس را به ورودی متد sum می فرستد حتما آن کلاس دارای متدی به نام area باشد.
و در نهایت ما در متد sum خود یک شرط از نوع بررسی interface قرار می دهیم تا بررسی صحت وجود متد area تکمیل شود.
1 2 3 4 5 6 7 8 9 10 | public function sum() { foreach($this->shapes as $shape) { if(is_a($shape, 'ShapeInterface')) { $area[] = $shape->area(); continue; } throw new AreaCalculatorInvalidShapeException; } return array_sum($area); } |
دیدید که چقدر interface کابردی است !
Liskov substitution principle
این اصل حاکی از آن است که کلاسهای فرزند باید آنقدر کامل و جامع از کلاس والد خود ارثبری کرده باشند که به سادگی بتوان همان رفتاری که با کلاس والد میکنیم را با کلاسهای فرزند نیز داشته باشیم به طوری که اگر در شرایطی قرار گرفتید که با خود گفتید کلاس فرزند میتواند تمامی کارهای کلاس والدش را انجام دهد به جزء برخی موارد خاص، اینجا است که این اصل از SOLID را نقض کردهاید.
یک کلاس به نام VolumeCalculator داریم که از AreaCalulator ارث بری کرده است.
1 2 3 4 5 6 7 8 9 | class VolumeCalculator extends AreaCalulator { public function construct($shapes = array()) { parent::construct($shapes); } public function sum() { // logic to calculate the volumes and then return and array of output return ($summedData); } } |
با اینکار ما ویژگی های AreaCalculator را به دست میاوریم و کلاس ها رفتاری شبیه به هم خواهند داشت ولی کاری که می کنند متفاوت است.
یک کلاس دیگر داریم که کارش نمایش دادن خروجی است اسم این کلاس SumCalculatorOutputter است.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class SumCalculatorOutputter { protected $calculator; public function __constructor(AreaCalculator $calculator) { $this->calculator = $calculator; } public function JSON() { $data = array( 'sum' => $this->calculator->sum(); ); return json_encode($data); } public function HTML() { return implode('', array( '', 'Sum of the areas of provided shapes: ', $this->calculator->sum(), '' )); } } |
کد بالا یک خروجی از نوع json به شکل Html برای ما چاپ می کند چرا حالا این همه کلاس ایجاد کردیم ؟!
علت ایجاد این همه کلاس به نحوه استفاده از آنها بر می گردد بخ نحوه احرای آنها نگاه کنید.
1 2 3 4 | $areas = new AreaCalculator($shapes); $volumes = new AreaCalculator($solidShapes); $output = new SumCalculatorOutputter($areas); $output2 = new SumCalculatorOutputter($volumes); |
می بینید که چقدر ساده توانستیم محیط مربوط به هر شئی را محاسبه کنیم علت تمامی کلاس های بالا ساخت یک ساختار ساده برای استفاده یا تغییر در آینده است.
Interface segregation principle
در قبل گفتیم که Interface اینترفیسها فقط مشخص میکنند که یک کلاس از چه متدهایی حتماً باید برخوردار باشد پس نتیجه میگیریم ایجاد چند interface بهتر از یک interface چند منظوره است اگر یک اینترفیس چندمنظورهٔ کامل و جامع داشته باشیم و سایر کلاسهای ما از آن اصطلاحاً implements
کنند، در چنین صورتی ممکن است برخی خصوصیات، متدها و رفتارها را به برخی کلاسهایی که اصلاً نیازی به آنها ندارند تحمیل کنیم اما اگر از چندین اینترفیس تخصصی استفاده کنیم، به سادگی میتوانیم از هر اینترفیسی که نیاز داشته باشیم در کلاسهای مد نظر خود استفاده نماییم.
کد زیر را مشاهده کنید.
1 2 3 4 | interface ShapeInterface { public function area(); public function volume(); } |
در اینجا یک interface ما دو منظور مختلف را همزمان دارد که اشتباه است و باید interface ها شکسته شده و به اجزای کوچکتر مثل زیر تبدیل شود.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | interface ShapeInterface { public function area(); } interface SolidShapeInterface { public function volume(); } class Cuboid implements ShapeInterface, SolidShapeInterface { public function area() { // calculate the surface area of the cuboid } public function volume() { // calculate the volume of the cuboid } } |
در بالا ما سه کلاس ایجاد کردیم که هر جا نیاز داشتیم از interface های تکی آنها استفاده می کنیم اگر نیاز شد که هردو interface باهم استفاده شوند یک interface جداگانه نیز برای اینکار ساخته شده است.
مثال ساده کاری که در بالا انجام شده است مثل این می ماند که شما بخواهید یک فیلد در پروژه خود را حذف کنید اگر از interface سوم (Cubiod) استفاده کرده باشید نیاز هست تا کل ساختار رو عوض کنید و دوباره کد نویسی کنید.
Dependency Inversion principle
قول مرحله آخر Solid ?
در مرحله Dependency Inversion principle از همه مراحل سخت تر است وبیشتر برنامه نویسان و توسعه دهندگان آن را با Dependency Injection اشتباه می گیرند (از این لینک می توانید در رابطه با Dependency Injection اطلاعات کسب کنید.)
کاری که باید انجام شود به زبان ساده این است که باید وابستگی میان کلاس های سطح بالا و سطح پایین را رعایت کنیم یعنی اگر در پروژه ای ماژول های سطح بالا و ماژول های سطح پایین وجود داشت نباید ماژول های سطح بالا وابسته به ماژوال های سطج پایین باشند بلکه باید وابسته به abstractions باشند. اگر متوحه نشدید به مثال زیر دقت کنید.
فرض کنید یک کلاس به نام PasswordReminder داریم که کارش یادآوری پسور به ماست.
1 2 3 4 5 6 | class PasswordReminder { private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->dbConnection = $dbConnection; } } |
در اینجا ما آمدیم و دیتایس رو صدا زدیم در صورتی که MySQLConnection یک ماژول low level است در صورتی که PasswordReminder یک ماژول top level است حالا سوال اینجاست چه مشکلی دارد همچین کاری انجام دهیم ؟
مشکل اونجایی پیش میاد که ما بخواهیم engine دیتابیس خود را تغییر دهیم به عنوان مثال الان از mysql استفاده می کنیم شاید بعدا نیاز بود که به Sqlserver تغییر پیدا کند.
اینطوری بگم نباید connection مربوط به دیتابیس را به آن شکل در آنجا صدا بزنیم خودش می تواند یک کلاس جداگانه باشد در واقع PasswordReminder نباید هیچ ارتباط مستقیمی با دیتابیس داشته باشد.
1 2 3 | interface DBConnectionInterface { public function connect(); } |
با ایجاد interface که یک نوع abstract است می توانیم یک متد به نام connect درست کنیم و فقط از همین متد در کلاس PasswordReminder استفاده کنیم.
1 2 3 4 5 6 7 8 9 10 11 | class MySQLConnection implements DBConnectionInterface { public function connect() { return "Database connection"; } } class PasswordReminder { private $dbConnection; public function __construct(DBConnectionInterface $dbConnection) { $this->dbConnection = $dbConnection; } } |
و اگر یک درصد بعدا نیاز بود که engine دیتابیس تغییر کند فقط کافی است کلاس مربوط به MySQLConnection ویرایش شود.
نتیجه گیری
صادقانه , شاید S.O.L.I.D در ابتدای کار خیلی کاربردی به نظر برسد اما با استفاده مداوم و پیروی از دستورالعمل های آن بخشی از شما و کد شما می شود که بدون هیچ مشکلی به راحتی قابل تمدید ، اصلاح ، آزمایش و اصلاح مجدد می باشد ( extended, modified, tested, and refactored )
شاید همیشه نیاز به استفاده از قوانیین SOLID نباشد ولی تجربه نشان داده اگر می خواهید پروژه های بزرگ توسعه دهید و قابلیت توسعه مجدد را برای دولوپر های دیگر قرار دهید حتما باید به این قوانین پایبند باشید.
یکی از سوالتی که حتما از شما در ورود به شرکت های بزرگ پرسیده خواهد شد همین اصول پنج گانه Solid است.
موفق و پیروز باشید.