درآمدی بر قانون Liskov Substitution

این اصل که توسط Barbara Liskov ابداع شده است حاکی از آن است که وقتی دست به نوشتن اینترفیس‌ها می‌زنیم، نه تنها باید روی نوع ورودی کلاس‌ ایمپلیمنت‌شده از روی یک اینترفیس دقت کنیم،‌ بلکه باید خروجی کلیهٔ کلاس‌هایی هم که از یک اینترفیس استفاده می‌کنند یکسان باشد. در یک کلام، ساب‌کلاس باید بتواند جایگزین سوپرکلاس شود بدون آنکه مشکلی ایجاد گردد. به عبارتی، Liskov Substitution Principle یا به اختصار LSP حاکی از آن است که هر گونه پیاده‌سازی Abstraction (انتزاع)، که یکی از اصول چهارگانهٔ OOP است، باید بدون آنکه باگی ایجاد شود در هر جایی استفاده شود. 

برای روشن‌تر شدن کاربر LSP،‌ در ادامه مثال کلاسیک مستطیل و مربع را در قالب کد توضیح خواهیم داد به طوری ساختار این پروژه به صورت زیر خواهد بود:

liskov-substitution-principle/
├── index.php
├── Quadrilateral.php
├── Rectangle.php
└── Square.php

لازم به یادآوری است که برای شلوغ نشدن سورس‌کدهای مورد استفاده در این آموزش، از نوشتن کلیهٔ تگ‌های آغازین php؟> خودداری کرده‌ایم. به منظور روشن شدن کاربرد SRP، ساختار پروژهٔ زیر را مد نظر قرار می‌دهیم:

ابتدا پوشه‌ای به نام liskov-substitution-principle ساخته سپس در فایلی تحت عنوان Rectangle.php کلاسی با نام Rectangle نوشته‌ایم:

class Rectangle
{
    protected $width;
    protected $height;
 
    public function setHeight($height)
    {
        $this->height = $height;
    }
 
    public function getHeight()
    {
        return $this->height;
    }
 
    public function setWidth($width)
    {
        $this->width = $width;
    }
 
    public function getWidth()
    {
        return $this->width;
    }
 
    public function area()
    {
         return $this->getHeight() * $this->getWidth();
    }
}

همان‌طور که می‌بینیم، داخل این کلاس دو پراپرتی تحت عناوین width$ و height$ تعریف کرده‌ایم سپس با استفاده از متدهای اصطلاحاً Setter آن‌ها را به ترتیب مقداردهی کرده و در فانکشن ()area نیز از متدهای به اصطلاح Getter استفاده نموده‌ایم تا مساحت مستطیل محاسبه گردد و در ادامه فایل دیگری ساخته‌ایم با نام Square.php که قرار است داخل آن کلاسی به اسم Sqaure بنویسیم که از کلاس Rectangle ارث‌بری می‌کند:

require_once 'Rectangle.php';
class Square extends Rectangle
{
    public function setHeight($value)
    {
        $this->width = $value;
        $this->height = $value;
    }
 
    public function setWidth($value)
    {
        $this->width = $value;
        $this->height = $value;
    }
}

با توجه به این که در هندسه مربع نوعی مستطیل تلقی می‌گردد، از همین روی ما هم به عنوان دولوپر فکر می‌کنیم که با ارث‌بری کردن از روی کلاس مرتبط با مستطیل می‌توانیم به هدف خود دست یابیم که حداقل روی کاغذ چنین چیزی مشکلی ایجاد نمی‌کند اما در ادامهٔ‌ آموزش خواهید دید که این‌طور نیست!

حال برای تست هر دو کلاس والد و فرزند، فایلی می‌سازیم به اسم index.php و آن را به صورت زیر تکمیل می‌کنیم:

require_once 'Rectangle.php';
require_once 'Square.php';

$rectangle = new Rectangle();
$rectangle->setHeight(2);
$rectangle->setWidth(3);
echo $rectangle->area();

echo '<br>';

$square = new Square();
$square->setHeight(2);
$square->setWidth(3);
echo $square->area();

در خطوط ابتدایی هر دو فایل را ایمپورت کرده‌ایم و در ادامه آبجکتی تحت عنوان rectangle$ از روی کلاس Rectangle ساخته و از طریق متدهای Setter این کلاس، width$ و height$ مستطیل را مقداردهی کرده‌ایم و در نهایت هم با استفاده از فانکشن ()area مساحت را در خروجی چاپ کرده‌ایم (با درج یک دستور <br> یک فاصله ایجاد کرده و همچون دستورات قبل، آبجکتی از روی کلاس Square ایجاد کرده‌ایم.) با اجرای این فایل در لوکال‌هاست با خروجی زیر مواجه خواهیم شد:

6
9

در تفسیر خروجی فوق می‌توان گفت که مساحت مستطیل به درستی برابر با ۶ محاسبه شده‌ است و در ارتباط با شیئ مربع نیز انتظار می‌رود که مساحت برابر با همان مقدار ۶ باشد اما در کمال تعجب می‌بینیم که مقدار ۹ به دست آمده است! برای درک بهتر این موضوع، مجدد کلاس Square را مد نظر قرار می‌دهیم:

require_once 'Rectangle.php';
class Square extends Rectangle
{
    public function setHeight($value)
    {
        $this->width = $value;
        $this->height = $value;
    }
 
    public function setWidth($value)
    {
        $this->width = $value;
        $this->height = $value;
    }
}

برای محاسبهٔ مساحت مربع باید هر دو پراپرتی‌ width$ و height$ را برابر با عدد واحدی قرار دهیم که در همین راستا داخل فانکشن ()setHeight مقدار هر دو پراپرتی را برابر با ۲ قرار داده‌ایم اما وقتی که فانکشن ()setWidth فراخوانی می‌شود، برای آن‌ها مقدار ۳ را در نظر گرفته می‌شود و چون کدها از بالا به پایین اجرا می‌شوند، همین می‌شود که خروجی ۹ مشاهده خواهد شد. حال بار دیگر کدهای فوق را به صورت زیر تغییر می‌دهیم:

$square = new Square();
$square->setHeight(3);
$square->setWidth(2);
echo $square->area();

در حقیقت فقط جای مقادیر ورودی برای width$ و height$ را تغییر داده‌ایم که در صورت اجرای اسکریپت فوق، با خروجی زیر مواجه خواهیم شد:

4

برای رفع این مشکل، قانون LSP را به صورت زیر اِعمال می‌کنیم:

interface Quadrilateral
{
    public function setHeight($h);
    public function setWidth($w);
    public function area();
}

در فایلی تحت عنوان Quadrilateral.php یک اینترفیس ساخته‌ایم تحت عنوان Quadrilateral به معنی «چهارضلعی» که در آن اصول کار تعریف شده است به طوری که هر دو کلاس مستطیل و مربع از آن ایمپلیمنت خواهند شد:

require_once 'Quadrilateral.php';
class Rectangle implements Quadrilateral
{
    protected $width;
    protected $height;
 
    public function setHeight($height)
    {
        $this->height = $height;
    }
 
    public function getHeight()
    {
        return $this->height;
    }
 
    public function setWidth($width)
    {
        $this->width = $width;
    }
 
    public function getWidth()
    {
        return $this->width;
    }
 
    public function area()
    {
         return $this->getHeight() * $this->getWidth();
    }
}

کلاس مربع نیز به صورت زیر تغییر می‌یابد:

require_once 'Quadrilateral.php';
class Square implements Quadrilateral
{
    protected $width;
    protected $height;
 
    public function setHeight($height)
    {
        $this->height = $height;
    }
 
    public function getHeight()
    {
        return $this->height;
    }
 
    public function setWidth($width)
    {
        $this->width = $width;
    }
 
    public function getWidth()
    {
        return $this->width;
    }
 
    public function area()
    {
         return $this->getHeight() * $this->getWidth();
    }
}

حال مجدد فایل index.php را مد نظر قرار می‌دهیم:

require_once 'Rectangle.php';
require_once 'Square.php';

$rectangle = new Rectangle();
$rectangle->setHeight(2);
$rectangle->setWidth(3);
echo $rectangle->area();

echo '<br>';

$square = new Square();
$square->setHeight(2);
$square->setWidth(3);
echo $square->area();

و به عنوان خروجی داریم:

6
6

و می‌بینیم خروجی مربع که قبلاً به اشتباه ۹ بازگردانده می‌شد با این ساختار مرتفع شده است.

جمع‌بندی
در واقع، مشکلی که گاهی در توسعهٔ نرم‌افزار مبتنی بر OOP پیش می‌آید این است که دولوپر نیاز دارد تا بسته به نوع خروجی شروط مختلفی در نظر بگیرد تا مثلاً اگر خروجی آرایه بود یک کار انجام شود و اگر آبجکت بود کاری دیگر و این در حالی است که اگر از قانون LSP پیروی کنیم، هندل کردن شروطی از این دست به سادگی امکان‌پذیر خواهد شد و این در حالی است که پیچیدگی سورس‌کد نیز به حداقل می‌رسد.

نظرات
اگر login نکردی برامون ایمیلت رو بنویس: