Можете ли вы объяснить принцип замены Лискова хорошим примером С#?

Можете ли вы объяснить принцип замены Лискова ( "L" SOLID) с хорошим примером С#, охватывающим все аспекты принципа в упрощенном виде? Если это действительно возможно.

Ответ 1

(Этот ответ был переписан 2013-05-13, читайте обсуждение внизу комментариев)

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

Перефразируя некоторые части, чтобы было понятнее.

Ответ 2

LSP - практический подход

Везде, где я ищу примеры LSP С#, люди использовали воображаемые классы и интерфейсы. Вот практическая реализация LSP, которую я реализовал в одной из наших систем.

Сценарий. Предположим, у нас есть 3 базы данных (клиенты по ипотечным кредитам, клиенты по текущим счетам и клиенты по сберегательному счету), которые предоставляют данные о клиентах, и нам нужны данные о клиентах для данной фамилии клиента. Теперь мы можем получить более одной информации о клиенте из этих 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 класса доступа к данным установлены, теперь мы обращаем наше внимание на клиента. На бизнес-уровне у нас есть класс 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

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

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 заявляет: "Производные классы должны заменять свои базовые классы (или интерфейсы)" & "Методы, использующие ссылки на базовые классы (или интерфейсы), должны иметь возможность использовать методы производных классов, не зная об этом или не зная деталей ".