Liskov Substitution Principle

اصول S.O.L.I.D در طراحی شی گرا (OOD)

تاریخ : یکشنبه 21 آبان 1396

مفهوم Liskov Substitution Principle

lsp به طور خلاصه این مفهوم را می رساند :

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

تعویض پذیری یک اصل در برنامه نویسی شی گرا است که در یک برنامه کامپیوتری ، اگر S یک زیرمجموعه از T باشد، پس از آن اشیاء نوع T ممکن است با اشیاء نوع S جایگزین شوند (یعنی یک شی از نوع T می تواند جایگزین هر هدف از یک زیر نوع S) بدون تغییر هیچ کدام از خواص مطلوب T .

به طور رسمی، اصل تعویض لیسکوف ( LSP ) یک تعریف خاص از رابطه زیر مجموعه است ( زیرمجموعه قوی) که در ابتدا توسط باربارا لیسکوف در سخنرانی یک کنفرانس در سال 1987 با نام انتزاع داده ها و سلسله مراتب معرفی شد . این یک رابطه معنایی است و نه صرفا نحوی ، زیرا در نظر دارد قابلیت همکاری معنایی انواع در یک سلسله مراتب را تضمین کند.

باربارا لیسکوف و ژنیت وینگ این اصل را به صورت خلاصه در مقاله ای در سال 1994 فرموله کردند:

Subtype Requirement: Let be a property provable about objects of type T. Then should be true for objects of type S where S is a subtype of T.

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

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->width * $this->height;
    }
}

کلاس Rectangle (مستطیل) را به عنوان یک کلاس پایه در نظر بگیرید. در دنیای واقعی شکل مربع هم نوعی مستطیل است. اما آیا در دنیای برنامه نویسی هم این موضوع صحیح است؟

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

    public function setWidth($value)
    {
        $this->width = $value;
        $this->height = $value;
    }
}

کلاس Square (مربع) از کلاس پایه ارث بری میکند. اما تفاوت این کلاس با کلاس پایه در این است که اگر هر یک از ابعاد طول و عرض مقدار دریافت کنند بعد دیگر نیز همان مقدار را خواهد داشت. یعنی اگر مقدار طول را 4 در نظر بگیریم, قطعا مقدار عرض نیز 4 خواهد بود.(خاصیت مربع)

حالا قطعه کد زیر را در نظر بگیرید:

function areaVerifier(Rectangle $obj)
{
    echo 'area = '.$obj->area();
}

$obj1 = new Rectangle();
$obj1->setWidth(5);
$obj1->setHeight(4);
areaVerifier($obj1);

echo '
---------------------------
'; $obj2 = new Square(); $obj2->setWidth(5); $obj2->setHeight(4); areaVerifier($obj2);

تابع areaVerifier ورودی از نوع Rectangle را دریافت کرده و طول و عرض آنرا مقدار دهی میکند. و در نهایت بررسی میکند که آیا مساحت درست حساب شده است یا خیر. در ظاهر همه چیز خیلی خوب کار خواهد کرد. اما وقتی کلاس Square که نوعی از Rectangle هست را به عنوان ورودی به تابع areaVerifier بدهیم پاسخ کمی تغییر می کند. وقتی به متد setHeight واقع در خط 15 می رسیم بر اساس کاری که Square انجام میدهد مقدار Width را نیز برابر ۴ قرار میدهد. بنابراین مساحت ۱۶ خواهد شد نه 20 (دقت کنید تابع areaVerifier شی از نوع Square دریافت کرده است اما رفتار از نوع شی Rectangle انتظار می رود اما مشاهده می کنیم رفتار توابع setWidth و setHeight کلاس والد خود را تغییر داده است).

// result
area is correct ---- area = 20
---------------------------
Bad area! ---- area = 16

خوب این به چه معنی است؟ به این معنی که ما نمیتوانیم کلاس پایه را با کلاس مشتق شده جایگزین کنیم و باز هم این معنی را میدهد که ما داریم اصل LSP را نقض میکنیم.

این موضوع هدف اصلی LSP است و به ما آموزش میدهد که هنگام ارث بری از یک کلاس نباید رفتار کلاس والد را تغییر دهیم.

اما راه حل چیست؟

یک کلاس انتزاعی (abstract) را به شکل زیر ایجاد و سپس دوکلاس Square و Rectangle را از آن مشتق میکنیم :

<?php
abstract class Shape
{
    protected $width;
    protected $height;
    abstract public function setHeight($height);
    abstract public function setWidth($width);
    public function getHeight()
    {
        return $this->height;
    }
    public function getWidth()
    {
        return $this->width;
    }
    public function area()
    {
        return $this->width * $this->height;
    }
}


class Rectangle extends Shape
{
    public function setWidth($width)
    {
        $this->width = $width;
    }
    public function setHeight($height)
    {
        $this->height = $height;
    }
}


class Square extends Shape
{
    public function setHeight($value)
    {
        $this->width = $value;
        $this->height = $value;
    }

    public function setWidth($value)
    {
        $this->width = $value;
        $this->height = $value;
    }
}

همچنین تابع areaVerifier را مطابق زیر تغییر می دهیم :

<?php
function areaVerifier(Shape $obj)
{
    echo 'area = '.$obj->area();
}

$obj1 = new Rectangle();
$obj1->setWidth(5);
$obj1->setHeight(4);
areaVerifier($obj1);

echo '
---------------------------
'; $obj2 = new Square(); $obj2->setWidth(5); $obj2->setHeight(4); areaVerifier($obj2);

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

area = 20
---------------------------
area = 16

منابع مورد مطالعه جهت جمع آوری این مطلب:
https://en.wikipedia.org/wiki/Liskov_substitution_principle
http://pikneek.com/programming/مفهوم-solid-در-برنامه-نویسی-lsp/
http://roocket.ir/articles/solid-principles-in-object-oriented-programming-part-iii-liskov-substitution-principal


نظرات