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

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


person pencilCake    schedule 13.12.2010    source источник
comment
Вот упрощенный способ думать об этом в двух словах: если я следую LSP, я могу заменить любой объект в моем коде фиктивным объектом, и ничего в вызывающем коде нужно будет скорректировать или изменить, чтобы учесть замену. LSP — это фундаментальная поддержка шаблона Test by Mock.   -  person kmote    schedule 09.01.2014
comment
В этом ответе есть еще несколько примеров соответствия и нарушений.   -  person StuartLC    schedule 06.10.2016


Ответы (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

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

person jgauffin    schedule 13.12.2010
comment
@jgauffin: пример простой и понятный. Но решение, которое вы предлагаете, во-первых: нарушает принцип открытости-закрытости и не соответствует определению дяди Боба (см. заключительную часть его статьи), в котором говорится: все программы, соответствующие принципу Open-Closed. см.:objectmentor.com/resources/articles/lsp.pdf - person pencilCake; 13.12.2010
comment
Я не вижу, как решение ломает Open/Closed. Прочитайте мой ответ еще раз, если вы имеете в виду часть if duck is ElectricDuck. В прошлый четверг у меня был семинар по SOLID :) - person jgauffin; 13.12.2010
comment
Не совсем по теме, но не могли бы вы изменить свой пример, чтобы вы не выполняли проверку типов дважды? Многие разработчики не знают о ключевом слове as, что на самом деле избавляет их от проверки типов. Я думаю примерно следующее: if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn(); - person Siewers; 22.08.2011
comment
@jgauffin - меня немного смущает пример. Я думал, что в этом случае принцип замещения Лискова все еще будет действителен, потому что Duck и ElectricDuck являются производными от IDuck, и вы можете поместить ElectricDuck или Duck в любое место, где используется IDuck. Если ElectricDuck должен включить перед тем, как утка сможет плавать, разве это не ответственность ElectricDuck или некоторого кода, создающего экземпляр ElectricDuck и затем устанавливающего для свойства IsTurnedOn значение true. Если это нарушает LSP, кажется, что LSV будет очень трудно придерживаться, поскольку все интерфейсы будут содержать различную логику для своих методов. - person Xaisoft; 24.10.2011
comment
LSP говорит, что все производные классы должны работать так, как указано в контракте базового класса (или интерфейса). В этом случае метод Swim должен заставить утку плавать. Это как если бы у вас был автомобильный интерфейс с методом Break, с которым класс SportsCar ничего не делал, потому что ломать в Ferrari не весело. Представьте себе удивление пользователя, когда он нажимает на педаль тормоза. - person jgauffin; 25.10.2011
comment
@jgauffin - я не согласен. в LSP нет требований, говорящих о том, что объект фактически должен выполнять одно и то же действие или вообще делать что-либо полезное. LSP говорит только об интерфейсе или контракте. LSP говорит, что вы можете относиться ко всему, что является уткой, как к утке, это не значит, что оно будет функционировать как утка, просто у него такой же интерфейс. Вы могли бы, например, заставить утку крякать каждый раз, когда вы вызываете Swim, и это не нарушит LSP, так как по договору она остается неизменной. - person Erik Funkenbusch; 12.05.2013
comment
Это правда, что LSP также говорит о поведенческих контрактах, но это относится к самому контакту. Примером является квадрат, полученный из прямоугольника. Принуждение размеров к одинаковым нарушает контракт о том, что Rectangle имеет независимо устанавливаемые размеры. Установка X=1 и Y=2, если X=2 не удастся (таким образом, контракт интерфейса был нарушен). Поскольку поведение Duck не является частью контракта, оно не подпадает под LSP. Если у вас есть метод, вызывающий Swim, и свойство IsSwimming, которое должно быть истинным (но не истинным), если вы вызываете Swim, то это нарушение. - person Erik Funkenbusch; 12.05.2013
comment
@MystereMan: имхо, LSP - это все о корректности поведения. В примере с прямоугольником/квадратом вы получаете побочный эффект установки другого свойства. С уткой вы получаете побочный эффект, что она не плавает. ЛСП: 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
comment
Вместо этого давайте возьмем реальный пример. У вас есть IUserRepository с методом сохранения. Метод сохранения отлично работает в репозитории по умолчанию (используя OR/M). Но когда вы измените его для использования службы WCF, он не будет работать для всех пользовательских объектов, которые не сериализуемы. Это заставляет приложение вести себя не так, как ожидалось. - person jgauffin; 13.05.2013
comment
@jgauffin - Ваш пример правильный, он нарушит LSP, но только потому, что LSP указывает, что вы не можете создавать новые исключения, которые не являются производными от каких-либо исходных исключений. Попытка сохранить несериализуемый объект вызовет ошибку сериализации. Если бы она не выдавала эту ошибку, несмотря на то, как работает программа, она не нарушала бы LSP. Корректность означает, что когда вы вызываете интерфейс, интерфейс не делает ничего неожиданного. Это не значит, что фактическое поведение объекта не может быть неожиданным, только интерфейс. - person Erik Funkenbusch; 13.05.2013
comment
@jgauffin - это тонкое различие, но важное. В противном случае LSP сделал бы полиморфизм практически бесполезным, поскольку замена любого объекта другим нарушила бы LSP, так как его поведение отличается от того, что изначально ожидалось автором кода. Поведение относится к договорному поведению, а не к фактическому поведению объекта. - person Erik Funkenbusch; 13.05.2013
comment
Я не согласен. Что я имею в виду в своем примере WCF, так это то, что новые репозитории просто игнорируют эти объекты. Ошибка не выдается. Это НАРУШИТ базовый контракт. Так же, как если бы в контракте IDuck указано, что Swim() всегда должен плавать. Если бы он не плавал, это нарушило бы базовый контракт. Исключения не имеют ничего общего с LSP. - person jgauffin; 13.05.2013
comment
Целью LSP является обеспечение того, чтобы все подклассы вели себя так, как указано в базовом классе. И это основная проблема полиморфизма. Можно наследовать классы, но не соблюдать отношения is-a. Это всегда вызывает побочные эффекты, поскольку базовый контракт не выполняется должным образом. - person jgauffin; 13.05.2013
comment
давайте продолжим это обсуждение в чате - person jgauffin; 13.05.2013
comment
@jgauffin Единственное, чего я не понимаю, это то, что принцип замещения Лисков заключается в использовании правильного типа абстракций для вашего класса. А в своем примере вы упомянули и органическую утку, и электрическую. Но разве пример не был бы лучше, если бы вы выбрали зеленую утку и красную утку? Потому что, если это выглядит как утка, крякает как утка, но нуждается в батареях - у вас, вероятно, неправильная абстракция. В любом случае, хороший пример! - person Giellez; 10.08.2017

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 и предоставляет свой объект базы данных.

Конечно, нам нужны идентичные хранимые процедуры во всех участвующих базах данных.

Наконец, клиент для CustomerServiceManagerclass будет вызывать только метод GetCustomerDetails, передавать lastName и не должен заботиться о том, как и откуда поступают данные.

Надеюсь, это даст вам практический подход к пониманию LSP.

person Yawar Murtaza    schedule 10.05.2016
comment
Как это может быть примером LSP? - person Roshan; 07.09.2017
comment
Я тоже не вижу здесь примера LSP... Почему у него так много голосов? - person StaNov; 09.11.2017
comment
@RoshanGhangare IDataAccess имеет 3 конкретные реализации, которые можно заменить на бизнес-уровне. - person Yawar Murtaza; 09.11.2017
comment
@YawarMurtaza, какой бы вы ни цитировали пример, является типичной реализацией шаблона стратегии, вот и все. Не могли бы вы пояснить, где нарушается LSP и как вы решаете это нарушение LSP? - person Yogesh; 14.12.2017
comment
@Yogesh - вы можете поменять местами реализацию IDataAccess с любым из его конкретных классов, и это не повлияет на клиентский код - вот что такое LSP в двух словах. Да, в некоторых шаблонах проектирования есть совпадения. Во-вторых, ответ выше предназначен только для того, чтобы показать, как LSP реализован в производственной системе для банковского приложения. В мои намерения не входило показать, как можно сломать LSP и как это исправить — это будет учебное пособие, и вы можете найти их сотни в Интернете. - person Yawar Murtaza; 01.10.2020

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

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

person mark333...333...333    schedule 11.04.2018