مرا اسکن کن!

معرفی Open Close Principle در قاعده SOLID

معرفی Open Close Principle در قاعده SOLID



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

به طور خلاصه: اصل SOLID برای نوشتن نرم افزارهایی با معماری خوب و منعطف برای تغییرات بعدی کاربرد دارد. نرم افزارهای متوسط و بزرگ به دلیل تغییرات زیاد و طول عمر بلند مدت تر، نیاز دارند تا از یک سری اصول مهندسی نرم افزار پیروی کنند. یکی از این اصول SOLID بود.

(اگر با مفهوم SOLID آشنایی ندارین پیشنهاد میکنم اول این پست را مطالعه کنید)

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

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

منظور از OCP این است که موجودیت های یک نرم افزار(کلاس ها، توابع و ماژول های مختلف) بایستی در برابر توسعه باز باشند و در برابر تغییرات بسته!

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

با کمی دقت متوجه می شویم که 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);

    }

}

 

همانطور که مشاهده می کنید در خط ۸ منطق محاسبه ی محیط را در قالب متد 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);

    }

}

 

در خط ۱ 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 را در کلاسمان ایجاد نماییم تا وابستگی را از بین ببریم .

مثال ۲ :

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

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 فایل دیگری مثل mp۳ به این بخش اضافه کنیم باید کلاس Progress را تغییر دهیم تا این نوع از فایل را نیز شناسایی کند.

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

 

اگر اصل بالا برایتان کمی پیچیده بود، مثالی دیگر برایتان داریم: فرض کنید یک کلاس(Class) نوشته اید و در آن کلاس نیز چند تابع(Function) دارید. چند ماهی از شروع توسعه نرم افزار گذشته است و حالا از تیم فروش به شما خبر میرسد که یک ویژگی اضافی احتیاج داریم. شما هم به سراغ کد میروید و میبینید که بایستی در همان کلاس الف قبلی، تابع ب را تغییر دهید. از طرفی هم میخواهید که کد قبلی شما دست نخورده باقی بماند تا ساختار کلی برنامه خراب نشود و یا به کد برنامه نویس قبلی دست نزده باشید! پس در واقع بین یک دو راهی گیر افتاده ایم: ۱. از طرفی نباید به کدهای قدیمی دست زد، زیرا ممکن است جاهای دیگر برنامه که از آن ها استفاده کرده اند خراب شود یا مشکلات دیگری به وجود بیاید ۲. میخواهیم یک قسمت جدید به تابع فعلی اضافه کنیم تا بتوانیم ویژگی مورد نظر تیم فروش را به نرم افزار اضافه کنیم.

برای این راهکار، اصل باز-بسته  به کار میرود و تکنیک های مختلفی که در این اصل وجود دارد. برای غلبه بر مشکل گفته شده در بالا میتوانید از سلسله مراتبی و Inheritance و یا الگو نرم افزار Composite استفاده کنید. ولی برای یک مثال ساده میتوان تکه کد زیر را در نظر گرفت(به زبان PHP)

 

class Teacher
{
    public function teach()
    {
        return 'teach';
    }
}
class Student
{
    public function study()
    {
        return 'study';
    }
}
class Manager
{
    public function process($member)
    {
        if ($member instanceof Teacher) {
            $member->teach();
        } elseif ($member instanceof Student) {
            $member->study();
        }
    }
}

 

همان طور که در کد بالا مشاهده میکنید، ما سه کلاس داریم. کلاس Teacher که آقای معلم است و کار تدریس را انجام می دهد. کلاس Student که درس میخواند و کلاس Manager که آقای مدیر است و وظیفه مدریت دو موجودیت دیگر را دارد. فرض کنید بعد از مدتی میخواهید یک موجودیت دیگر به نام آقای ناظم به سیستم اضافه کنید که عملیات نظم دادن را انجام میدهد. حتما میدانید که برای این کار علاوه بر ساخت کلاس Monitor که همان آقای ناظم است، باید در کلاس Manger یک if دیگر بگذارید تا چک کند که اگر کلاسی که به مدیر گفته شده از نوع آقای ناظر است، آن وقت تابع نظم دادن را صدا بزند. طبق اصل باز-بسته بودن نرم افزار نباید کلاس Manger دست بخورد. پس تکه کد زیر که تغییر یافته کد بالاست را نگاهی بیندازید:

interface Workable
{
    public function work();
}
class Teacher implements Workable
{
    public function work()
    {
        return 'teach';
    }
}
class Student implements Workable
{
    public function work()
    {
        return 'study';
    }
}
class Manager
{
    public function process(Workable $member)
    {
        return $member->work();
    }
}

 

کمی در کد بالا فکر کنید. یک Interface داریم که دو کلاس آقای معلم و دانش آموز از نوع آن هستند. حتما میدانید که کلاس هایی که از نوع یک Interface حتما باید تمامی توابع آن Interface را داشته باشند. هر کدام از کلاس های آقای معلم و دانش آموز یک تابع work دارند. حال کلاس آقای مدیر را نگاه کنید. این کلاس فقط کافی است که از متغیر member که از نوع Interface Workable است تابع work را صدا بزند.(اگر با کاربرد Interface آشنایی ندارید مثال آن را در گوگل سرچ کنید). حال اگر به کلاس آقای مدیر یک متغیر از نوع آقای معلم نسبت داده شود(یا هر کلاس دیگری) فرقی ندارد، زیرا آقای مدیر فقط تابع work مربوط به آن کلاس را صدا میزند. حتما متوجه شده اید که برای اضافه کردن یک کلاس آقای ناظم فقط کافیست این کلاس از نوع interface Workable باشد و تابع work داشته باشد. آقای ناظم کار خود را در تابع work باید انجام دهد. با این کار دیگر نیازی نیست که در کلاس Manger تغییری ایجاد کنیم.

استفاده از مفهوم Open-closed ساختار برنامه را با کیفیت تر می کند و به برنامه نویسان اجازه میدهد که کدهای خود را به راحتی و بدون توجه به پیامدهای مختلف تغییرات بنویسند. کد نویسی نیز تمیزتر و خواناتر شده و تغییرات بعدی را ساده تر می سازد.


نوشته شده توسط :

وحید صمدیان وحید صمدیان



پنجشنبه, 19 مهر 1397

تعداد بازديد : 805

برچسب ها : الگوی طراحی Design Pattern

3.0 ستاره