Можете ли вы объяснить принцип подстановки Лискова (буква «L» SOLID) на хорошем примере C #, охватывающем все аспекты этого принципа в упрощенной форме? Если это действительно возможно.
Можете ли вы объяснить принцип подстановки Лискова (буква «L» SOLID) на хорошем примере 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();
}
Но это нарушит принцип открытости / закрытости и должно быть реализовано повсюду (и поэтому по-прежнему генерирует нестабильный код).
Правильным решением было бы автоматически включить утку в 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 - практический подход
Везде, где я ищу примеры LSP C #, люди использовали воображаемые классы и интерфейсы. Вот практическая реализация LSP, которую я реализовал в одной из наших систем.
Сценарий: предположим, что у нас есть 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 для всех трех баз данных, который расширяется каждым из классов баз данных, как показано ниже.
ИПОТЕКА ДОСТУП К ДАННЫМ КЛИЕНТА:
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 гласит: «Производные классы должны быть заменяемыми для своих базовых классов (или интерфейсов)» & «Методы, которые используют ссылки на базовые классы (или интерфейсы), должны иметь возможность использовать методы производных классов, не зная об этом или не зная подробностей. . "