سولید شامل 5 نکته برای طراحی و برنامه نویسی OOP ئه. اگر این نکات ساده رو رعایت کنیم توسعه و نگهداری برای دولوپر بسیار راحت میشه. همچنین سولید باعث میشه تا برنامه نویس ها مرتکب - code smell - نشن ، کد ها رو خیلی سریع ری فاکتور کنن و همچنین بخشی از 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(
            "",
                "Sum of the areas of provided shapes: ",
                $this->sum(),
            ""
        ));
    }
}

 

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

$shapes = array(
    new Circle(2),
    new Square(5),
    new Square(6)
);

$areas = new AreaCalculator($shapes);

echo $areas->output();

 

مشکل با تابع output اینه که کلاس AreaCalulatorوظیفه محسابه مساحت رو برعهده داره، حالا اگه فک نفر بخواد از مساحت خروجی جیسان بگیره تکلیف چیه؟ در حال حاضر همه این وظایف بر عهده AreaCalulatorکه مخالف اصل اول سولیده. طبق اصل اول سولید وظیفه 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 های بیشتری رو به متد اضافه بکنیم و این بر خلاف اصل دوم سولیده !

یک راه میتونه این باشه که منطق محاسبه مساحت رو از کلاس 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هم میتونیم چک کنیم که اگر کلاس داده شده از جنس این اینترفیس بود که هیچی، اگر نه وارد یک 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 "Database connection";
    }
}

class PasswordReminder {
    private $dbConnection;

    public function __construct(DBConnectionInterface $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
}

 

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

 

نتیجه گیری 

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