Чи можете ви пояснити принцип заміщення Ліскова на гарному прикладі C #? [зачинено]


92

Чи можете ви пояснити Принцип заміщення Ліскова ("L" ТВЕРДОГО) на гарному прикладі C #, що спрощує всі аспекти принципу? Якщо це дійсно можливо.


9
Ось спрощений спосіб думати про це в двох словах: якщо я слідую LSP, я можу замінити будь-який об’єкт у своєму коді об’єктом Mock, і ніщо у викличному коді не потрібно буде коригувати або змінювати з урахуванням заміни. LSP є фундаментальною підтримкою шаблону Test by Mock.
kmote

Є ще кілька прикладів відповідності та порушення в цій відповіді
StuartLC

Відповіді:


128

(Ця відповідь була переписана 13.05.2013, прочитайте обговорення внизу коментарів)

LSP - це дотримання контракту базового класу.

Наприклад, ви не можете кидати нові винятки в підкласи, оскільки той, що використовує базовий клас, цього не очікував. Те саме стосується і базового класуArgumentNullException якщо аргумент відсутній, а підклас дозволяє аргументу бути нульовим, що також є порушенням LSP.

Ось приклад структури класу, який порушує LSP:

public interface IDuck
{
   void Swim();
   // contract says that IsSwimming should be true if Swim has been called.
   bool IsSwimming { get; }
}

public class OrganicDuck : IDuck
{
   public void Swim()
   {
      //do something to swim
   }

   bool IsSwimming { get { /* return if the duck is swimming */ } }
}

public class ElectricDuck : IDuck
{
   bool _isSwimming;

   public void Swim()
   {
      if (!IsTurnedOn)
        return;

      _isSwimming = true;
      //swim logic            
   }

   bool IsSwimming { get { return _isSwimming; } }
}

І телефонний код

void MakeDuckSwim(IDuck duck)
{
    duck.Swim();
}

Як бачите, є два приклади качок. Одна органічна качка та одна електрична качка. Електрична качка може плавати, лише якщо вона ввімкнена. Це порушує принцип LSP, оскільки його потрібно вмикати, щоб плавати якIsSwimming (що також є частиною контракту) не буде встановлено, як у базовому класі.

Звичайно, ви можете вирішити це, роблячи щось подібне

void MakeDuckSwim(IDuck duck)
{
    if (duck is ElectricDuck)
        ((ElectricDuck)duck).TurnOn();
    duck.Swim();
}

Але це порушить принцип Open / Closed і повинен бути впроваджений скрізь (і все ще генерує нестабільний код).

Правильним рішенням було б автоматичне ввімкнення качки в Swimметоді і, завдяки цьому, електрична качка поводилась точно так, як це визначено IDuckінтерфейсом

Оновлення

Хтось додав коментар і видалив його. У ньому був дійсний пункт, до якого я хотів би звернутися:

Рішення з включенням качки всередину Swimметоду може мати побічні ефекти при роботі з реальною реалізацією ( ElectricDuck). Але це можна вирішити, використовуючи явну реалізацію інтерфейсу . imho, швидше за все у вас виникають проблеми, якщо НЕ вмикати його, Swimоскільки очікується, що він буде плавати під час використанняIDuck інтерфейсу

Оновлення 2

Переформулював деякі частини, щоб зробити це більш чітким.


1
@jgauffin: Приклад простий і зрозумілий. Але рішення, яке ви пропонуєте, по-перше: порушує принцип відкритості та закритості, і воно не відповідає визначенню дядька Боба (див. Підсумкову частину його статті), яке пише: "Принцип заміщення Ліскова (AKA Design by Contract) є важливою особливістю усіх програм, які відповідають принципу відкрито-закрито ". див .: objectmentor.com/resources/articles/lsp.pdf
олівець Торт

1
Я не бачу, як рішення розбиває Open / Closed. Прочитайте мою відповідь ще раз, якщо ви маєте на увазі if duck is ElectricDuckчастину. У мене був семінар про SOLID минулого четверга :)
jgauffin

Не зовсім по темі, але чи можете ви, будь ласка, змінити приклад, щоб не робити перевірку типу двічі? Багато розробників не знають asключового слова, що насправді рятує їх від великої кількості перевірок типу. Я думаю приблизно наступне:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
Siewers

3
@jgauffin - мене трохи бентежить приклад. Я думав, що принцип заміщення Ліскова все ще буде дійсним у цьому випадку, оскільки Duck та ElectricDuck походять від IDuck, і ви можете поставити ElectricDuck або Duck у будь-якому місці, де використовується IDuck. Якщо ElectricDuck потрібно ввімкнути, перш ніж качка зможе плавати, хіба це не відповідальність ElectricDuck або якогось коду, що створює екземпляр ElectricDuck, а потім встановлює властивість IsTurnedOn на true. Якщо це порушує LSP, здається, що LSV буде дуже важко дотримуватися, оскільки всі інтерфейси містять різну логіку для своїх методів.
Xaisoft,

1
@MystereMan: imho LSP - це все про коректність поведінки. На прикладі прямокутника / квадрата ви отримуєте побічний ефект від іншого властивості, яке встановлюється. З качкою ви отримуєте побічний ефект не плавання. LSP:if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).
jgauffin

8

LSP - практичний підхід

Скрізь, де я шукаю приклади C # LSP, люди використовували уявні класи та інтерфейси. Ось практична реалізація LSP, яку я впровадив в одній із наших систем.

Сценарій: Припустимо, у нас є 3 бази даних (іпотечні клієнти, клієнти поточних рахунків та клієнти ощадного рахунку), які надають дані клієнта, і нам потрібні дані клієнта для даного прізвища клієнта. Тепер ми можемо отримати більше 1 інформації про клієнта з цих 3 баз даних із зазначеним прізвищем.

Реалізація:

БІЗНЕС-МОДЕЛЬ ШАР:

public class Customer
{
    // customer detail properties...
}

ШАР ДОСТУПУ ДАНИХ:

public interface IDataAccess
{
    Customer GetDetails(string lastName);
}

Вище інтерфейс реалізований абстрактним класом

public abstract class BaseDataAccess : IDataAccess
{
    /// <summary> Enterprise library data block Database object. </summary>
    public Database Database;


    public Customer GetDetails(string lastName)
    {
        // use the database object to call the stored procedure to retrieve the customer details
    }
}

Цей абстрактний клас має загальний метод "GetDetails" для всіх 3 баз даних, який розширений кожним з класів бази даних, як показано нижче

ДОСТУП ДО ДАНИХ КЛІЄНТА ІПОТЕКИ:

public class MortgageCustomerDataAccess : BaseDataAccess
{
    public MortgageCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetMortgageCustomerDatabase();
    }
}

ПОТОЧНИЙ ДОСТУП ДО ДАНИХ КЛІЄНТІВ:

public class CurrentAccountCustomerDataAccess : BaseDataAccess
{
    public CurrentAccountCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetCurrentAccountCustomerDatabase();
    }
}

ЕКОНОМІЯ ДОСТУП ДО ДАНИХ КЛІЄНТІВ:

public class SavingsAccountCustomerDataAccess : BaseDataAccess
{
    public SavingsAccountCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetSavingsAccountCustomerDatabase();
    }
}

Після встановлення цих 3 класів доступу до даних ми звертаємо нашу увагу на клієнта. На рівні Business ми маємо клас CustomerServiceManager, який повертає дані про клієнта своїм клієнтам.

БІЗНЕС ШАР:

public class CustomerServiceManager : ICustomerServiceManager, BaseServiceManager
{
   public IEnumerable<Customer> GetCustomerDetails(string lastName)
   {
        IEnumerable<IDataAccess> dataAccess = new List<IDataAccess>()
        {
            new MortgageCustomerDataAccess(new DatabaseFactory()), 
            new CurrentAccountCustomerDataAccess(new DatabaseFactory()),
            new SavingsAccountCustomerDataAccess(new DatabaseFactory())
        };

        IList<Customer> customers = new List<Customer>();

       foreach (IDataAccess nextDataAccess in dataAccess)
       {
            Customer customerDetail = nextDataAccess.GetDetails(lastName);
            customers.Add(customerDetail);
       }

        return customers;
   }
}

Я не показував введення залежності, щоб зробити це простим, оскільки це вже ускладнюється зараз.

Тепер, якщо ми маємо нову базу даних про клієнта, ми можемо просто додати новий клас, який розширює BaseDataAccess і забезпечує його об’єкт бази даних.

Звичайно, нам потрібні однакові збережені процедури у всіх базах даних, що беруть участь.

Нарешті, клієнт для CustomerServiceManagerкласу буде викликати лише метод GetCustomerDetails, передавати lastName і не повинен дбати про те, як і звідки надходять дані.

Сподіваюся, це дасть вам практичний підхід до розуміння LSP.


3
Як це може бути приклад LSP?
somegeek

1
Я теж не бачу прикладу LSP у цьому ... Чому у нього так багато голосів?
StaNov

1
@RoshanGhangare IDataAccess має 3 конкретні реалізації, які можна замінити на бізнес-рівні.
Явар Муртаза

1
@YawarMurtaza, що б ви не цитували, це типова реалізація шаблону стратегії. Чи можете ви пояснити, де це порушує LSP і як ви вирішуєте це порушення LSP
Yogesh

0

Ось код застосування принципу заміщення Ліскова.

public abstract class Fruit
{
    public abstract string GetColor();
}

public class Orange : Fruit
{
    public override string GetColor()
    {
        return "Orange Color";
    }
}

public class Apple : Fruit
{
    public override string GetColor()
    {
        return "Red color";
    }
}

class Program
{
    static void Main(string[] args)
    {
        Fruit fruit = new Orange();

        Console.WriteLine(fruit.GetColor());

        fruit = new Apple();

        Console.WriteLine(fruit.GetColor());
    }
}

LSV стверджує: "Похідні класи повинні бути замінюваними своїми базовими класами (або інтерфейсами)" & "Методи, які використовують посилання на базові класи (або інтерфейси), повинні мати можливість використовувати методи похідних класів, не знаючи про це або знаючи подробиці . "

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.