bagher2g
bagher2g
خواندن ۱۰ دقیقه·۵ سال پیش

آشنایی با S.O.L.I.D


راستش دنبال یک فرصت شغلی جدید بودم که با یک چیز جالب مواجه شدم، تو اکثر آگهی های کاریابی "آشنایی با solid" رو دیدم و تصمیم گرفتم تا یکم درباره این چارچوب تحقیق کنم. نتیجه تحقیقاتم که ترجمه چند تا مقاله از سایت های مختلف بود رو با شما به اشتراک میذارم.

اصول S.O.L.I.D

سولید شامل 5 نکته برای طراحی و برنامه نویسی OOP ئه. اگر این نکات ساده رو رعایت کنیم توسعه و نگهداری برای دولوپر بسیار راحت میشه. همچنین سولید باعث میشه تا برنامه نویس ها مرتکب - code smell - نشن ، کد ها رو خیلی سریع refactor کنن و همچنین استفاده از این دیزاین پترن بخشی از agile  و متد توسعه نرم افزاره.

S.O.L.I.D بر پایه اصول زیره

  • S - Single-responsiblity principle
  • O - Open-closed principle
  • L - Liskov substitution principle
  • I - Interface segregation principle
  • D - Dependency Inversion Principle

بیاید بررسی کنیم ببینیم هر کدوم از این نکات به طور فردی چه کاری انجام میده تا در نهایت بفهمیم سولید چجوری ما رو تبدیل به برنامه نویسای بهتری میکنه

Single-responsibility Principle

هر کلاس تنها باید یک وظیفه داشته باشد

برای مثال فرض کنید ما یک سری شی داریم و میخوایم مساحت کل اشیا رو حساب کنیم! تا اینجاش که ساده بود درسته؟

class Circle { public $radius; public function construct($radius) { $this->radius = $radius; } } class Square { public $length; public function construct($length) { $this->length = $length; } }

در مرحله اول کلاس شی هامونو میسازیم و براشون کانستراکتور درست میکنیم

در مرحله بعد یک کلاس AreaCalulatorدرست میکنیم و منطق محاسبه مساحت اشیا رو براش مینویسم

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( &quot&quot, &quotSum of the areas of provided shapes: &quot, $this->sum(), &quot&quot )); } }

برای استفاده از این کلاس به راحتی یک نمونه از اون رو میسازیم و آرایه ای از اشیا رو بهش پاس میدیم بعد هم خروجی رو چاپ میکنیم

$shapes = array( new Circle(2), new Square(5), new Square(6) ); $areas = new AreaCalculator($shapes); echo $areas->output();

مشکل با تابع output اینه که کلاس AreaCalulatorوظیفه محسابه مساحت رو برعهده داره، حالا اگه فک نفر بخواد از مساحت خروجی جیسان بگیره تکلیف چیه؟ در حال حاضر همه این وظایف بر عهده AreaCalulatorکه مخالف اصل اول solid هست. طبق اصل اول solid وظیفه AreaCalulatorباید تنها و تنها محاسبه مساحت باشه. برای این کلاس نباید مهم باشه که کاربر چه خروجی ای میخواد؛ جیسان یا Html.

برای حل این مشکل میتونیم یک کلاس SumCalculatorOutputter  طراحی کنیم و با این کلاس هندل کنیم که مساحت ها چجوری و با چه فرمتی نمایش داده بشن.

$shapes = array( new Circle(2), new Square(5), new Square(6) ); $areas = new AreaCalculator($shapes); $output = new SumCalculatorOutputter($areas); echo $output->JSON(); echo $output->HAML(); echo $output->HTML(); echo $output->JADE();

Open-closed Principle

این اصل اشاره میکنه که هر کلاس باید به سادگی قابل توسعه باشه بدون اینکه نیاز باشه به اون کلاس دست بزنیم و یا تغییراتی توش اعمال کنیم.

بیاید به کلاس AreaCalulatorبه خصوص متد sum یه نگاهی بندازیم

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); }

اگر بخوایم یه کاری کنیم که متد sum مساحت اشیای بیشتری رو محاسبه کنه باید if/else های بیشتری رو به متد اضافه کنیم و این بر خلاف اصل دوم solid هست!

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

class Square { public $length; public function __construct($length) { $this->length = $length; } public function area() { return pow($this->length, 2); } }

دقیقا همینکار رو باید برای Circle انجام بدیم، یک متد area باید اضافه بشه حالا محاسبه مساحت اشیا میتونه به راحتی شبیه کد زیر باشه

public function sum() { foreach($this->shapes as $shape) { $area[] = $shape->area(); } return array_sum($area); }

حالا میتونیم هر شی دیگه ای رو بسازیم و بدون اینکه به کدمون دست بزنیم مساحت اون رو محاسبه کنیم! حالا از کجا بفهمیم آبجکتی که به داخل AreaCalulatorپاس داده میشه یک Shapeئه و از کجا بدونیم اگر Shape ئه متد area رو داره؟

استفاده از interface بخش کلیدی و مهمی از اصول solidئه ؛ راه حل منطقی اینه که یک اینترفیس بسازیم و هر کلاس shape اون اینترفیس رو ایمپلمنت کنه

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); } }

تو کلاس AreaCalulatorهم میتونیم چک کنیم که اگر کلاس داده شده از جنس این interface بود که هیچی، اگر نه وارد یک Exception بشه .

public function sum() { foreach($this->shapes as $shape) { if(is_a($shape, 'ShapeInterface')) { $area[] = $shape->area(); continue; } throw new AreaCalculatorInvalidShapeException; } return array_sum($area); }

Liskov substitution principle

اگر q(x) یک Attribute از ابجکت x که از کلاس T هست باشه اون موقع q(y) که یک attr از کلاس S هست و کلاس S از کلاس T ارث میبره باید درست باشه !

خیلی خب یک بار دیگه !

هر زیر کلاس باید بتونه یک جایگزین برای کلاس پدر یا والد خودش باشه !

فرض کنید ما یک VolumeCalculator داریم که از کلاس AreaCalulator ارث میبره

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 array($summedData); } }

در کلاس SumCalculatorOutputter  داریم :

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(), '' )); } }

اگر بخوایم با مثال جلو بریم همچین چیزی میشه :

$areas = new AreaCalculator($shapes); $volumes = new AreaCalculator($solidShapes); $output = new SumCalculatorOutputter($areas); $output2 = new SumCalculatorOutputter($volumes);

برنامه ران میشه اما وقتی میخوایم از $output2 خروجی Html بگیریم با E_NOTICE رو به رو میشیم !

برای حل این مشکل به جای اینکه تو متد sum از VolumeCalculator  آرایه برگردونیم باید به طور ساده به شکل زیر عمل کنیم که خروجی شبیه متد sum توی AreaCalculator بشه به این ترتیب این کلاس میتونه تو کل کدها جایگزین کلاس AreaCalculator باشه

function sum() { // logic to calculate the volumes and then return and array of output return $summedData; }

Interface segregation principle

کلاینت هرگز نباید مجبور بشه تا interfaceی رو implement کنه که بهش نیاز نداره و ازش استفاده نمیکنه یا نباید به همچین اینترفیسی اصلا نیاز داشته باشه و وابسته باشه .

ازاونجایی که ما هنوز از Shape ها استفاده میکنیم و ازاونجایی که اشیا حجیم داریم و از اونجایی که میخواهیم حجم هرکدوم از این اشیا رو به دست بیاریم متد volume رو به اینترفیسمون اضافه میکنیم

interface ShapeInterface { public function area(); public function volume(); }

حالا همه اشیا ما باید این اینترفیس رو پیاده کنن! اما ما میدونیم که دایره مسطحه و حجم نداره . پس بنابراین این اینترفیس دایره رو مجبور میکنه تا متدری رو پیاده سازی کنه که بهش نیاز نداره .

این قضیه مخالف اصل چهارم solidئه ، به جای اینکار شما میتونید یک اینترفیس تعریف کنید به اسم SolidShapeInterface  و اشیایی که حجم دارن مثل مکعب از این اینترفیس استفاده کنن

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 } }

Dependency Inversion principle

موجودیت ها باید به کلیات وابسته باشن نه جزئیات ، در واقع این اصل داره میگه که ماژول های high level نباید به ماژول های low level وابستگی داشته باشن.

شاید یکم بد گفته باشم ولی این اصل واقعا آسونه! خب این اصل رو با یک مثال توضیح میدیم

class PasswordReminder { private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->dbConnection = $dbConnection; } }

اول اینکه MysqlConnection یک ماژول low-level ئه اما PasswordReminder یک ماژول high-level ئه و این مخالف اصل پنجم سولید هست . در اینجا کلاس PasswordReminder مجبور شده تا از کلاس MysqlConnection استفاده کنه

جدا از اون اگر شما بخواید دیتابیستون رو عوض کنید باید کلاس PasswordReminder رو دستکاری کنید که این مخالف اصل دوم هست ( گسترش بدون دست بردن توی کد )

در واقعا کلاس PasswordReminder اصلا نباید براش مهم باشه که شما از چه دیتابیسی استفاده میکنید برای اینکار دوباره برمیگردیم به اینترفیس و یک اینترفیس طراحی میکنیم

interface DBConnectionInterface { public function connect(); }

حالا DBConnectionInterfaceیک متد داره به اسم connect و جدا از اینکه دیتابیسمون چی باشه PasswordReminder میتونه به دیتابیس وصل شه

class MySQLConnection implements DBConnectionInterface { public function connect() { return &quotDatabase connection&quot } } class PasswordReminder { private $dbConnection; public function __construct(DBConnectionInterface $dbConnection) { $this->dbConnection = $dbConnection; } }

طبق کد بالا میتونید ببینید که چه ماژول های high level و چه ماژول های low level به کلیات وابسته اند و نه جزئیات .

نتیجه گیری

در نگاه اول Solid بسیار اذیت کننده به نظر میرسه اما به مرور زمان و کار کردن باهاش میشه بخشی از فرایند کد زدنتون. در نتیجه کد شما رو توسعه پذیر و تست پذیر میکنه .


برنامه نویسیoopsoliddesign pattern
شاید از این پست‌ها خوشتان بیاید