آشنایی با اصول .S.O.L.I.D (بخش سوم - LSP):


با عرض سلام و احترام.
پیشاپیش از شما دوست عزیز و گرامی، بابت وقتی که برای مطالعه ی این مطلب خواهید گذاشت، سپاسگزارم.
تقاضا دارم، در صورت مشاهده ی اشتباه متنی یا محتوایی، به اینجانب اطلاع دهید تا (ضمن کمک به یادگیری بنده) در اسرع وقت برای اصلاح متن اقدام نمایم.
شماره ی تماس:
09215149218
نشانی پست الکترونیکی:
RezaQadimi.ir@Gmail.com
آدرس سایت ها:
https://Reza-Qadimi.ir - https://WannaDate.ir

هدف من در این مقاله، آشنایی شما با سومین اصل از اصول .S.O.L.I.D، یعنی LSP یا Liskov Substitution Principle است.


اصل Liskov Substitution، یکی دیگر از اصول SOLID است که اولین بار در سال 1988، توسط Barbara Liskov معرفی شد.

موضوع این اصل رابطه والد-فرزندی، یا به عبارتی ویژگی ارث بری/Inheritance در Object Oriented Design می باشد.

این اصل بیان می کند:

"فرض کنید کلاس B از کلاس A ارث بری کرده است. با توجه به این موضوع، هر آبجکتی از کلاس B باید بتواند بدون تحت تاثیر قرار دادن عملکرد و صحتِ "پیاده سازی" یا "برنامه"، جایگزین آبجکتی از کلاس A شود."



به کد زیر توجه کنید:

public interface ISavingAccount
{
        //other method and property...

        bool Withdrawal(decimal amount);
}


  • اینترفیس "ISavingAccount"، بر اساس حساب های مختلف معرفی شده توسط بانک (مانند Regular، Salary و FixDeposit) پیاده سازی شده است.
public class RegularSavingAccount : object, ISavingAccount
{
        public RegularSavingAccount() : base()
        {
        }

        //other method and property and code...

        public bool Withdrawal(decimal amount)
        {
                decimal moneyAfterWithdrawal = Balance - amount;

                if (moneyAfterWithdrawal >= 1000)
                {
                        return true;
                }
                else
                {
                        return false;
                }
        }
}


public class SalarySavingAccount : object, ISavingAccount
{
        public SalarySavingAccount() : base()
        {
        }

        //other method and property and code...

        public bool Withdrawal(decimal amount)
        {
                decimal moneyAfterWithdrawal = Balance - amount;

                if (moneyAfterWithdrawal >= 0)
                {
                        return true;
                }
                else
                {
                        return false;
                }
        }
}


  • اما با توجه به قوانین بانک، حساب FixDeposit امکان برداشت وجه را ندارد (در صورتی که در رابطه باقی حساب ها این امکان وجود دارد):
public class FixDepositSavingAccount : object, ISavingAccount
{
        public FixDepositSavingAccount() : base()
        {
        }

        //other method and property and code...

        public bool Withdrawal(decimal amount)
        {
                string errorMessage =
                       $ &quotNot supported by { this } account type!&quot

                throw new System.Exception(message: errorMessage);
        }
}

  • حال میخواهیم متد "Withdrawal" پیاده سازی شده در کلاس های ارث بری کرده از اینترفیس "ISavingAccount" را فراخوانی کنیم:
public class AccountManager : object
{
        public AccountManager() : base()
        {
        }

        public bool
                WithdrawalFromAccount(
                decimal amount, ISavingAccount account)
        {
                bool result = account.Withdrawal(amount: amount);

                return result;
        }
}

  • نتیجه ی فراخوانی متد WithdrawalFromAccount با پارامتر ورودی از جنس اینترفیس ISavingAccount:
OK:

ISavingAccount account =
        new RegularSavingAccount ();

AccountManager.WithdrawalFromAccount(amount: amount, account: account);


OK:

ISavingAccount account =
        new RegularSavingAccount ();

AccountManager.WithdrawFromAccount(amount: amount, account: account);


Runtime Error:

ISavingAccount account =
        new FixeDepositeSavingAccount();

AccountManager.WithdrawFromAccount(amount: amount, account: account);

مثال بالا، نمونه ای از نقض اصل Liskov Substitution می باشد! چرا که کلاس FixDepositSavingAccount رفتار تابع Withdrawal مربوط به والد خود را نقض کرده است. اما چه باید کرد؟

بار دیگر Liskov Substitution Principle را با هم مرور میکنیم:

کلاس فرزند، نباید functionality والد خود را نقض کند، و آبجکت ساخته شده از کلاس فرزند باید بتواند در هر زمانی جایگزین آبجکت ساخته شده از کلاس والد شود، بدون این که بر عملکرد و یا صحت آن آسیبی وارد نماید.

  • برای برطرف کردن این مشکل، ما دو کلاس جدید، با نام های "SavingAccountWithWithdrawal" و "SavingAccountWithoutWithdrawal" میسازیم که از اینترفیس "ISavingAccount" ارث بری کرده اند.
public interface ISavingAccount
{
}


  • همانطور که ملاحظه میکنید، به جای قرار دادن متد Withdrawal در اینترفیس "ISavingAccount"، آن را در کلاس "SavingAccountWithWithdrawal" قرار داده ایم، و کلاس هایی که از آن ارث بری میکنند را مجبور به پیاده سازی این متد می نماییم:
public abstract class
        SavingAccountWithWithdrawal: object, ISavingAccount
{
          public SavingAccountWithWithdrawal() : base()
          {
          }

          public abstract bool Withdrawal(decimal amount);
}


public abstract class
        SavingAccountWithoutWithdrawal : object, ISavingAccount
{
        public SavingAccountWithoutWithdrawal() : base()
        {
        }
}

public class RegularSavingAccount : SavingAccountWithWithdrawal
{
        public RegularSavingAccount() : base()
        {
        }

        //other method and property and code...

        public override bool Withdrawal(decimal amount )
        {
                // implementation...
        }
 }


public class SalarySavingAccount : SavingAccountWithWithdrawal
{
        public SalarySavingAccount() : base()
        {
        }

         //other method and property and code...

        public override bool Withdrawal(decimal amount )
        {
                  // implementation...
        }
}


  • از آنجا که امکان "برداشت وجه" از حساب "Fix Deposit" را نداریم، نیازی به پیاده سازی متد "Withdrawal" نیز نخواهیم داشت! بنابراین کلاس مربوط به پیاده سازی منطق این نوع حساب را، از کلاس "SavingAccountWithoutWithdrawal" ارث بری مینماییم:


public class FixDepositSavingAccount : SavingAccountWithoutWithdrawal 
{
        public FixDepositSavingAccount () : base()
        {
        }
}

  • این بار به جای قرار دادن پارامتر ورودی متد "WithdrawalFromAccount" از جنس "IServiceAccount"، آن را از جنس "SavingAccountWithWithdrawal" قرار میدهیم:
public class AccountManager : object
{
        public AccountManager() : base()
        {
        }

        public bool
                WithdrawalFromAccount(
                decimal amount, SavingAccountWithWithdrawal account)
        {
                bool result = account.Withdrawal(amount: amount);

                return result;
        }
}

با توجه به این موضوع، از این به بعد میتوانیم اطمینان داشته باشیم که پارامتر account پاس داده شده به این تابع از جنس "SavingAccountWithWithdrawal" بوده و متد "Withdrawal" را پیاده سازی کرده است و در صورت ارسال پارامتری که با واسطه یا بی واسطه از این کلاس ارث بری نکرده باشد، در زمان کامپایل با خطا مواجه میشویم (نه Runtime).


  • نتیجه ی فراخوانی متد "WithdrawFromAccount" کلاس "AccountManager":
OK:

ISavingAccount account =
        new RegularSavingAccount ();

AccountManager.WithdrawFromAccount(amount: amount, account: account );


OK:

ISavingAccount account =
        new RegularSavingAccount ();

AccountManager.WithdrawFromAccount(amount: amount, account: account);


Compile Error:

ISavingAccount account =
        new FixDepositSavingAccount();

AccountManager.WithdrawFromAccount(amount: amount, account: account);

همانطور که میبینید، در صورت فراخوانی متد "WithdrawFromAccount" کلاس "AccountManager"، با ورودی از جنس "FixDepositSavingAccount"، کامپایلر به ما خطا خواهد داد.


معایب عدم رعایت Liskov Substitution Principle:

  1. نقض رفتار والد توسط فرزند.
  2. امکان raise شدن Runtime Error، و بروز خطا در زمان اجرای برنامه.

پی نوشت: در مقاله ی بعد، به بررسی اصل Interface Segregation Principle خواهیم پرداخت.


معرفی:
رضا قدیمی هستم. برنامه نویس و دانش آموزِ حوزه ی وب، بسیار مشتاق در یادگیری مفاهیم و اطلاعات جدید در این حوزه.