Можете ли вы объяснить принцип замещения Лискова («L» в слове SOLID) на хорошем примере C#, охватывающем все аспекты этого принципа в упрощенной форме? Если это действительно возможно.
Можете ли вы объяснить принцип подстановки Лискова на хорошем примере С#?
Ответы (3)
(Этот ответ был переписан 13 мая 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 :)
- person jgauffin; 13.12.2010
as
, что на самом деле избавляет их от проверки типов. Я думаю примерно следующее: if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
- person Siewers; 22.08.2011
Break
, с которым класс SportsCar
ничего не делал, потому что ломать в Ferrari не весело. Представьте себе удивление пользователя, когда он нажимает на педаль тормоза.
- person jgauffin; 25.10.2011
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).
- person jgauffin; 13.05.2013
IUserRepository
с методом сохранения. Метод сохранения отлично работает в репозитории по умолчанию (используя OR/M). Но когда вы измените его для использования службы WCF, он не будет работать для всех пользовательских объектов, которые не сериализуемы. Это заставляет приложение вести себя не так, как ожидалось.
- person jgauffin; 13.05.2013
is-a
. Это всегда вызывает побочные эффекты, поскольку базовый контракт не выполняется должным образом.
- person jgauffin; 13.05.2013
LSP: практический подход
Везде, где я ищу примеры LSP C#, люди используют воображаемые классы и интерфейсы. Вот практическая реализация 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» для всех трех баз данных, который расширяется каждым из классов базы данных, как показано ниже.
ДОСТУП К ДАННЫМ ИПОТЕЧНОГО КЛИЕНТА:
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 класса доступа к данным установлены, теперь мы обращаем внимание на клиента. На бизнес-уровне у нас есть класс 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
class будет вызывать только метод 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 заявляет: «Производные классы должны заменять свои базовые классы (или интерфейсы)» и «Методы, использующие ссылки на базовые классы (или интерфейсы), должны иметь возможность использовать методы производных классов, не зная об этом или не зная деталей. ."