سرفصل‌های آموزشی
آموزش قوانین SOLID
درآمدی بر قانون Liskov Substitution

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

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

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

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

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

ابتدا پوشه‌ای به نام 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->setWidth(3);
echo $rectangle->area();

echo '<br>';

$square = new Square();
$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 $side;

    public function setHeight($height)
    {
        $this->side = $height;
    }

    public function setWidth($width)
    {
        $this->side = $width;
    }
                
    public function getSide()
    {
    	return $this->side;
    }

    public function area()
    {
        return pow($this->side, 2);
    }
}

حال مجدد فایل 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
9

در مثالی دیگر که پایبند به این اصل است، یک Super Class به نام Bird داریم که متد walk را پیاده‌سازی می‌کند:

class Bird {
    protected string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function walk() {
        return "{$this->name} is walking.";
    }
}

همچنین یک کلاس به نام FlyingBird داریم که کلاس Bird را extend کرده و یک متد جدید به نام fly را نیز به آن اضافه می‌کند:

class FlyingBird extends Bird {
    protected int $flySpeed;

    public function __construct(string $name, int $flySpeed)
    {
        parent::__construct($name);
        $this->flySpeed = $flySpeed;
    }

    public function fly() {
        return "{$this->name} is flying at a speed of {$this->flySpeed}.";
    }
}

اکنون دو کلاس جدید تعریف می‌کنیم که هر کدام از کلاس‌های بالا را extend کنند و بالتبع متدهای نوشته در آن را نیز در خود دارند:

class Dove extends FlyingBird {
    protected string $color;

    public function __construct(string $name, int $flySpeed, string $color)
    {
        parent::__construct($name, $flySpeed);
        $this->color = $color;
    }
}

class Penguin extends Bird {
          protected string $color;

    public function __construct(string $name, string $color)
    {
        parent::__construct($name);
        $this->color = $color;
    }
}

بر اساس قانون Liskov، باید قادر باشیم تا شئ‌ای از سوپرکلاس Bird را با شئ‌ای از ساب‌کلاس‌های آن، که شامل کلاس‌های FlyingBird، Penguin، و Dove می‌شود، جا به جا کنیم بدون آن که برنامه دچار مشکل شود.

برای مثال تصور کنید تابعی به شکل زیر در برنامه داریم که در پارامتر ورودی، شئ‌ای از نوع Bird را می‌پذیرد(که به این معناست که هر شئ‌ای که Bird را extend کند نیز می‌پذیرد):

function birdWalking(Bird $bird) {
          return $bird->walk();
}

اکنون باید بتوانیم بدون این که اشکالی در روند برنامه رخ دهد، یک شئ از ساب‌کلاس Bird را به جای شئ‌ای از خود این سوپرکلاس، به این تابع بفرستیم.

به مثال زیر دقت نمایید:

function birdWalking(Bird $bird) {
          return $bird->walk();
}

$bird = new Bird('Birdie');
$flyingBird = new FlyingBird("Hawk", 5000);
$dove = new Dove("Rock dove", 3000, "Pale grey");
$penguin = new Penguin("Emperor penguin", "black-white");

echo birdWalking($bird); // output: Birdie is walking.
echo birdWalking($flyingBird); // output: Hawk is walking.
echo birdWalking($dove); // output: Rock dove is walking.
echo birdWalking($penguin); // output: Emperor penguin is walking

در فراخوانی تابع بالا با اشیاء مختلف، هیچ خطایی در برنامه ایجاد نشد و همه‌ی آن‌ها خروجی مرتبط را به درستی برگرداندند، که این نشان دهنده پایبندی به قانون Liskov است.

همین عمل را می‌توانید با سوپرکلاس FlyingBird و ساب‌کلاس‌ آن یعنی Dove انجام دهید. برنامه شما باید بتواند یک شئ از کلاس Dove را جایگزین یکی شئ از کلاس FlyingBird کند.

جمع‌بندی

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

online-support-icon