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


93

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


9
Вот упрощенный способ думать об этом в двух словах: если я буду следовать LSP, я могу заменить любой объект в моем коде объектом Mock, и ничего в вызывающем коде нужно будет скорректировать или изменить, чтобы учесть замену. LSP - это фундаментальная поддержка шаблона Test by Mock.
kmote

Ответы:


128

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

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


1
@jgauffin: Пример простой и понятный. Но решение, которое вы предлагаете, первое: нарушает принцип открытости-закрытости и не соответствует определению дяди Боба (см. Заключительную часть его статьи), в котором говорится: «Принцип замещения Лискова (также известный как« Дизайн по контракту ») - важная особенность. всех программ, которые соответствуют принципу «Открыто-Закрыто». см .: objectmentor.com/resources/articles/lsp.pdf
PencilCake

1
Я не вижу, как решение ломается Open / Closed. Прочтите мой ответ еще раз, если вы имеете в виду if duck is ElectricDuckчасть. В прошлый четверг у меня был семинар по SOLID :)
jgauffin

Не совсем по теме, но не могли бы вы изменить свой пример, чтобы не выполнять проверку типов дважды? Многие разработчики не знают о asключевом слове, что на самом деле избавляет их от множества проверок типов. Я думаю примерно следующее:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
Siewers

3
@jgauffin - меня немного смущает этот пример. Я думал, что принцип замещения Лискова будет по-прежнему применим и в этом случае, потому что Duck и ElectricDuck оба являются производными от IDuck, и вы можете поместить ElectricDuck или Duck везде, где используется IDuck. Если ElectricDuck необходимо включить, прежде чем утка сможет плавать, это не является обязанностью ElectricDuck или какого-либо кода, создающего экземпляр ElectricDuck и затем устанавливающего для свойства IsTurnedOn значение true. Если это нарушает LSP, кажется, что LSV будет очень трудно придерживаться, поскольку все интерфейсы будут содержать разную логику для его методов.
Xaisoft

1
@MystereMan: imho LSP - это правильность поведения. В примере прямоугольника / квадрата вы получаете побочный эффект другого установленного свойства. С уткой вы получаете побочный эффект - она ​​не плавает. 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).
jgauffin

9

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.


3
Как это может быть примером LSP?
somegeek

1
Я не вижу в этом примера LSP ... Почему за него так много голосов?
StaNov

1
@RoshanGhangare IDataAccess имеет 3 конкретных реализации, которые можно заменить на бизнес-уровне.
Явар Муртаза

1
@YawarMurtaza, какой бы пример вы ни цитировали, это типичная реализация шаблона стратегии. Не могли бы вы прояснить, где это нарушение LSP и как вы решаете это нарушение LSP?
Йогеш

@Yogesh - вы можете поменять местами реализацию IDataAccess на любой из его конкретных классов, и это не повлияет на клиентский код - вот что в двух словах представляет собой LSP. Да, в некоторых шаблонах проектирования есть совпадения. Во-вторых, приведенный выше ответ предназначен только для того, чтобы показать, как LSP реализован в производственной системе для банковского приложения. В мои намерения не входило показать, как можно нарушить LSP и как это исправить - это было бы учебное пособие, и вы можете найти их сотни в Интернете.
Явар Муртаза,

0

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.