Чи можете ви пояснити Принцип заміщення Ліскова ("L" ТВЕРДОГО) на гарному прикладі C #, що спрощує всі аспекти принципу? Якщо це дійсно можливо.
Чи можете ви пояснити Принцип заміщення Ліскова ("L" ТВЕРДОГО) на гарному прикладі C #, що спрощує всі аспекти принципу? Якщо це дійсно можливо.
Відповіді:
(Ця відповідь була переписана 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
Переформулював деякі частини, щоб зробити це більш чітким.
if duck is ElectricDuck
частину. У мене був семінар про SOLID минулого четверга :)
as
ключового слова, що насправді рятує їх від великої кількості перевірок типу. Я думаю приблизно наступне:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
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).
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.
Ось код застосування принципу заміщення Ліскова.
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 стверджує: "Похідні класи повинні бути замінюваними своїми базовими класами (або інтерфейсами)" & "Методи, які використовують посилання на базові класи (або інтерфейси), повинні мати можливість використовувати методи похідних класів, не знаючи про це або знаючи подробиці . "