آموزش Open Close Principle در SOLID

  • خانه
  • آموزش Open Close Principle در SOLID
Image تحقیقات

آموزش Open Close Principle در SOLID

سلام خدمت شما دوستان عزیز

در ادامه مبحث SOLID نوبت میرسه به معرفی دومین اصل  یعنی Open Close Principle که به صورت کوتاه OCP  هم می گویند.(اگر با مفهوم SOLID آشنایی ندارین پیشنهاد میکنم اول این پست را مطالعه کنید)

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

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(), "

"
        ));
    }
}

 

همانطور که در جلسه قبلی هم ذکر شد : یک کلاس به نام 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 را به خوبی رعایت میکند. البته برخی ترجیح میدهند به جای استفاده از اینترفیس از یک کلاس انتزاعی استفاده کنند. بستگی به نوع الگوی طراحی دارد که انتخاب میکنند. در نهایت این روش باعث میشود که بدون تغییر کلاس اصلی بتوان انواع فایل های دیگر را به پروژه اضافه کرد.