عنوان مقاله :

اصول SOLID در برنامه نویسی شی گرا قسمت دوم : Open Close Principle

گردآوری و تالیف : محمد صادق زحمتکش
تاریخ انتشار : 07 شهریور 1396
دسته بندی ها : پی اچ پی

در این مقاله در ادامه ی مبحث SOLID قصد داریم به معرفی دومین اصل  یعنی Open Close Principle که به صورت کوتاه OCP هم می گویند،بپردازیم.با ما همراه باشید:

تعریف را اینگونه بیان کرده اند :

Objects or entities should be open for extension, but closed for modification.

منظور از OCP این است که برنامه نویس بایستی کلاس ها و اشیاء را طوری ایجاد نماید که همواره امکان گسترش (Extend) آن وجود داشته باشد اما برای گسترش نیازی به تغییر در اصله کلاس  نباشد .

یعنی اینکه کدها و کلاس ها بایستی همواره برای گسترش و اکستند شدن باز باشند اما برای ویرایش و تغییر بسته باشند. البته منظور از بسته بودن برای ویرایش این است که نیازی به تغییر کد ها نباشد. در بهترین حالت این اصل در برنامه نویسی شی گرا باعث کاهش ارتباطات کلاس ها و ماژول ها خواهد شد. در نهایت امکان توسعه از طریق افزودن کلاس های جدید و نه تغییر کلاس های موجود میسر خواهد شد.

با کمی دقت متوجه می شویم که OCP و SRP مکمل همدیگر هستند. البته به این معنی نیست که هر جا SRP را رعایت کرده باشیم پس OCP را نیز رعایت کرده ایم. اما با رعایت هر یک از اصول دستیابی به مورد دیگر راحت تر و ساده تر خواهد بود.

به مثال زیر توجه کنید :


class AreaCalculator {

    protected $shapes;

    public function __construct($shapes = array()) {

        $this->shapes = $shapes;

    }


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

}

    public function output() {

        return implode('', array(

            "

", "Sum of the areas of provided shapes: ", $this->sum(), "

PHP

"

        ));

    }

}

همانطور که در جلسه قبلی هم ذکر شد : یک کلاس به نام AreaCalculator داریم که  مسؤل محاسبه ی جمع , محیط های اشکال هندسی هست .

اگر بخواهیم در آینده , متد sum , محیط های اشکال بیشتر و متنوع تری را باهم جمع کند نیاز است تا از if/else blocks های بیشتری استفاده کنیم زیرا فعلا فقط به Square و Circle اشاره کردیم . این اضافه کردن if/else blocks در واقع برخلاف اصل Open Close Principle است .پس راه حل چیست ؟

یکی از راه های خوبی که می توانیم این مشکل را حل کنیم این است که منطق محاسباتی محیط ها را از متد sum جدا کنیم و به کلاس های خود اشکال هندسی ببریم .مثلا برای

کلاس Square :

class Square {

    public $length;

    public function __construct($length) {

        $this->length = $length;

    }

    public function area() {

        return pow($this->length, 2);

    }

}

همانطور که مشاهده می کنید در خط 8 منطق محاسبه ی محیط را در قالب متد area  به کلاس square اضافه کردیم . همین کار را باید برای کلاس Circle انجام دهیم .وقتی منطق

محاسبه ی محیط را به داخل کلاس های اشکال بردیم , متد sum ای که در AreaCalculator  قرار داشت را  به صورت زیر تغییر می دهیم :

public function sum() {

    foreach($this->shapes as $shape) {

        $area[] = $shape->area;

    }

    return array_sum($area);
}

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

حالا یه مشکل دیگه ؟ از کجا متوجه شیم کلاسی که به AreaCalculator پاس داده می شود کلاسی است که مربوط به یک shape است و متد area را دارد ؟

برای برطرف کردن این مشکل فقط کافی است یک interface بسازیم و کلاس های مورد نظر که اشکال ما هستند را از این interface در واقع implements نمایم.

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

    }

}

در خط 1 interface را تعریف کردیم و در ادامه کلاس هایمان را از این اینترفیس implements کردیم تا مجبور شود متد area و بقیه چیزهای لازم را داشته باشد .

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

public function sum() {

    foreach($this->shapes as $shape) {

        if(is_a($shape, 'ShapeInterface')) {

            $area[] = $shape->area();

            continue;

        }

        throw new AreaCalculatorInvalidShapeException;

    }

    return array_sum($area);

}

توانستیم اصل Open Close Principle را در کلاسمان ایجاد نماییم تا وابستگی را از بین ببریم .

مثال 2 :

به عنوان مثال کلاس های زیر را در نظر بگیرید :

class TXTFile {

    public $length;

    public $sent;
}



class Progress {

    private $file;

    function __construct(TXTFile $file) {

        $this->file = $file;

    }

    function getAsPercent() {

        return $this->file->sent * 100 / $this->file->length;

    }

}

این روش کد نویسی اصل  OCP را نقض می کند. چرا؟ علت این است که کلاس Progress متغیر file از نوع TXTFile را به عنوان ورودی دریافت می کند و نوع دیگری از فایل ها را نمی شناسد. پس اگر در آینده بخواهیم به جز فایل txt فایل دیگری مثل mp3 به این بخش اضافه کنیم باید کلاس Progress را تغییر دهیم تا این نوع از فایل را نیز شناسایی کند.

شاید بگویید که خوب اگر نوع متغیر را در ورودی Progress مشخص نکنیم, میتوانیم در آینده کلاس دیگری مانند mp3 را به عنوان ورودی برای Progress ارسال کنیم. یعنی کد ما به شکل زیر شود :

class Progress {

    private $file;

    function __construct($file) {

        $this->file = $file;

    }

    function getAsPercent() {

        return $this->file->sent * 100 / $this->file->length;

    }

}

اما این هم قابل قبول نیست. اول اینکه دیباگ کردن این کد کمی سخت است. چرا؟ برای اینکه ممکن ورودی اشتباهی (مثلا یک رشته) برای Progress ارسال شود. و خطایی که دریافت می کنید چیزی مشابه زیر خواهد بود:

Trying to get property of non-object.

یعنی اینکه متغیر sent یا length را از چیزی غیر از یک کلاس(در اینجا رشته) می خواهید دریافت کنید که این اشتباه است و باعث خطا می شود. خوب دو چیز را باید بررسی کنید. اول اینکه مجبورید به کلاس Progress سری بزنید تا ببینید چه کدی در حال اجرا شدن است که چنین خطایی می دهد. دوم اینکه تشخیص اینکه آیا نوع متغیر ورودی اشتباه بوده یا اینکه متغیر ورودی مقدار اشتباهی دارد نیاز به بررسی دارد.

اما اگر نوع ورودی مشخص باشد, مانند کلاس Progress که در ابتدا نوشتیم امکان ارسال ورودی اشتباه وجود ندارد و خطای واضحی مشخص می کند که نوع ورودی اشتباه است. بنابراین دیباگ کردن در مرحله اول راحت تر می شود و علاوه بر آن انجام unit test بر روی این کلاس آسان تر خواهد بود.

پس به این نتیجه می رسیم که حذف کردن نوع ورودی از متد سازنده کلاس Progress بهترین کار نیست. روش دیگری که می توانیم انجام دهیم این است که مشخص کنیم تمام ورودی هایی که به کلاس Progress خواهیم داد از یک استاندارد پیروی می کنند. وقتی صحبت از استاندارد بین کلاس ها می شود معمولا پای یک اینترفیس یا یک کلاس انتزاعی(abstract) در میان است!

interface MeasurableInterface {

    function getLength();

    function getSent();

}

class File implements MeasurableInterface {

    private $length;

    private $sent;



    public $filename;

    public $owner;

    function setLength($length) {

        $this->length = $length;

    }

    function getLength() {

        return $this->length;

    }

    function setSent($sent) {

        $this->sent = $sent;

    }

    function getSent() {

        return $this->sent;

    }

    function getRelativePath() {

        return dirname($this->filename);

    }

    function getFullPath() {

        return realpath($this->getRelativePath());

    }

}

class Progress {

    private $measurableContent;

    function __construct(MeasurableInterface $measurableContent) {

        $this->measurableContent = $measurableContent;

    }

    function getAsPercent() {

        return $this->measurableContent->getSent() * 100 / $this->measurableContent->getLength();

    }

}

این روش در حال حاضر بهترین روشی است که هم SRP و هم OCP را به خوبی رعایت می کند. البته برخی ترجیح می دهند به جای استفاده از اینترفیس از یک کلاس انتزاعی استفاده کنند. بستگی به نوع الگوی طراحی دارد که انتخاب می کنند. در نهایت این روش باعث میشود که بدون تغییر کلاس اصلی بتوان انواع فایل های دیگر را به پروژه اضافه کرد.

مقالات پیشنهادی

برنامه نویسی شی گرا در php | قسمت دوم

برای اضافه کردن اطلاعات در کلاس ها از property ها استفاده میشه . کار اونها دقیقا شبیه متغیرها در php معمولیه و تنها تفاوتشون اینکه قبل از تایپ اسم pro...

برنامه نویسی شی گرا در php | قسمت پنجم

خب امروز در این پست رسیدیم به متدهایی جادوئی در شی گرایی php . در php برای ساده تر کردن بعضی از کارها در کلاس ها یک سری متدهای خاص و ویژه در شی گرایی...

برنامه نویسی شی گرا در php | قسمت چهارم

امروز میخوام در مورد یک بحث مهم در شی گرائی صحبت کنم و اونم وارثته . خوب اینجا من گفتم وارثت ، اولین چیزی که بعد از شنیدن وراثت به ذهنتون خطور کرد چی...

برنامه نویسی شی گرا در php | قسمت اول

در گذشته قبل از اینکه مفهومم شی گرایی وارد زبان php بشه برنامه نویس ها مجبور بودن که php رو در کنار html استفاده کنن که این روش در پروژهای کوچیک مشکل...

دیدگاه های ارزشمند شما

برای ارسال نظر لازم است ابتدا وارد سایت شوید
احسان | 3 ماه پیش

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

mehdi | 4 ماه پیش

سلام
ضمن خسته نباشید در مقاله های چند بخشی اگه در هر قسمت لینک سایر قسمتها را هم بزارید خیلی ممنون میشیم

حسام موسوی | 4 ماه پیش

سلام مرسی از نظرتون حتما اینکارو میکنیم

ایمان | 4 ماه پیش

خیلی خوب و کامل بود
احسنت