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

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

3 ответа

Решение

(Этот ответ был переписан 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

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

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

Везде, где я ищу примеры C# 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.

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

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

Каждый! Будет ли это решением ответа @jgauffin, изложенного выше?

public interface IDuck
{
    void Swim();
    bool IsSwimming { get; }
    bool IsReadyToSwim { get; } //I added checking which is applied to any descendant
}

public class Duck : IDuck
{
    bool _isSwimming;
    public bool IsSwimming => _isSwimming; 

    public virtual bool IsReadyToSwim => true;

    public void SwimIfReady()
    {
        if (IsReadyToSwim)
            Swim();
    }

    public void Swim()
    {
        _isSwimming = true;
    }
}

public class OrganicDuck : Duck
{
    public override bool IsReadyToSwim => true;
}
public class ElectricDuck : Duck
{
    public bool IsTurnedOn { get; set; }
    public override bool IsReadyToSwim => IsTurnedOn;
}
Другие вопросы по тегам