Как соблюдать Принцип замещения Лискова (LSP) и по-прежнему пользоваться полиморфизмом?
LSP говорит: "Производные типы не должны изменять поведение базовых типов", другими словами "Производные типы должны быть полностью заменены для базовых типов".
Это означает, что если мы определяем виртуальные методы в наших базовых классах, мы нарушаем этот принцип.
Также, если мы спрячем метод в методе диска с помощью нового ключевого слова, мы снова нарушили этот принцип.
Другими словами, если мы используем полиморфизм, мы нарушили LSP!
Во многих приложениях я использовал виртуальные методы в базовых классах, и теперь я понимаю, что это нарушает LSP. Также, если вы используете шаблон шаблона, вы нарушили этот принцип, который я использовал его много.
Итак, как разработать приложение, которое соответствует этому принципу, когда вам нужно наследование, и вы также хотите воспользоваться полиморфизмом? Я в замешательстве!
См. пример отсюда: http://www.oodesign.com/liskov-s-substitution-principle.html
Ответы
Ответ 1
LSP говорит, что вы должны иметь возможность использовать производный класс так же, как вы используете его суперкласс: "объекты в программе должны быть заменены экземплярами своих подтипов, не изменяя правильность этой программы". Классическое наследование, которое нарушает это правило, выводит квадратный класс из класса Rectangle, поскольку первый должен иметь Height = Width
, а последний может иметь Height != Width
.
public class Rectangle
{
public virtual Int32 Height { get; set; }
public virtual Int32 Width { get; set; }
}
public class Square : Rectangle
{
public override Int32 Height
{
get { return base.Height; }
set { SetDimensions(value); }
}
public override Int32 Width
{
get { return base.Width; }
set { SetDimensions(value); }
}
private void SetDimensions(Int32 value)
{
base.Height = value;
base.Width = value;
}
}
В этом случае поведение свойств Width и Height изменилось, и это является нарушением этого правила. Возьмем вывод, чтобы увидеть, ПОЧЕМУ изменилось поведение:
private static void Main()
{
Rectangle rectangle = new Square();
rectangle.Height = 2;
rectangle.Width = 3;
Console.WriteLine("{0} x {1}", rectangle.Width, rectangle.Height);
}
// Output: 2 x 2
Ответ 2
У Барбары Лисков есть хорошая статья Абстракция и иерархия данных, где она специально затрагивает полиморфное поведение и конструкции виртуального программного обеспечения. После прочтения этой статьи вы можете видеть, что она глубоко описывает, как программный компонент может достичь гибкости и модульности от простых полиморфных вызовов.
LSP указывает на детали реализации, а не на абстракции. В частности, если вы используете некоторый интерфейс или абстракцию типа T
, вы должны ожидать передачи всех подтипов T
, а не для наблюдения за неожиданным поведением или сбоем программы.
Ключевое слово здесь неожиданно, поскольку оно может описывать любые свойства вашей программы (правильность, выполняемая задача, возвращаемая семантика, временно и т.д.). Таким образом, создание методов virtual
не означает само по себе нарушение LSP
Ответ 3
Я думаю, что принцип замещения Лискова (LSP) в основном связан с перемещением реализации функций, которые могут отличаться от классов children, и оставлять родительский класс как можно более общим.
Итак, что бы вы ни изменили в дочернем классе, он не нарушает принцип замены Лискова (LSP), если это изменение не заставляет вас изменять код в родительском классе.
Ответ 4
"Производные типы не должны изменять поведение базовых типов" означает, что должно быть возможно использовать производный тип, как если бы вы использовали базовый тип. Например, если вы можете позвонить x = baseObj.DoSomeThing(123)
, вы также должны будете позвонить x = derivedObj.DoSomeThing(123)
. Полученный метод не должен генерировать исключение, если базовый метод этого не сделал. Код, использующий базовый класс, должен хорошо работать с производным классом. Он не должен "видеть", что использует другой тип. Это не означает, что производный класс должен делать то же самое; это было бы бессмысленно. Другими словами, использование производного типа не должно прерывать код, который работал плавно, используя базовый тип.
В качестве примера допустим, что вы объявили регистратор, позволяющий вам регистрировать сообщение на консоли
logger.WriteLine("hello");
Вы можете использовать инъекцию конструктора в классе, который должен создавать журналы. Теперь вместо того, чтобы передавать его в консольный логгер, вы передаете ему регистратор файлов, полученный из консольного регистратора. Если регистратор файлов генерирует исключение: "Вы должны указать номер строки в строке сообщения", это приведет к потере LSP. Однако не проблема, что запись идет в файл вместо консоли. То есть если регистратор показывает то же поведение для вызывающего, все в порядке.
Если вам нужно написать код, похожий на следующий, то LSP будет нарушен:
if (logger is FileLogger) {
logger.Write("10 hello"); // FileLogger requires a line number
// This throws an exception!
logger.Write("hello");
} else {
logger.Write("hello");
}
Кстати: ключевое слово new
не влияет на полиморфизм, вместо этого он объявляет совершенно новый метод, который имеет то же имя, что и метод в базовом типе, но не связан с ним. В частности, нельзя назвать его базовым типом. Чтобы полиморфизм работал, вы должны использовать ключевое слово override
, и метод должен быть виртуальным (если вы не реализуете интерфейс).
Ответ 5
Подтипы должны быть заменены базовыми типами.
В терминах контактов.
Производный класс может заменить предварительное условие базового класса для того же или более слабого и пост-состояния для того же или большего.
Ссылка
Ответ 6
Чтобы полиморфизм работал, необходимо соблюдать LSP. Прекрасным способом его разбить было бы введение методов в производный тип, которые не относятся к базовому типу. В этом случае полиморфизм не может работать, потому что эти методы недоступны в базовом типе. У вас может быть другая реализация подтипа метода, в то же время придерживаясь как полиморфизма, так и LSP.