Принцип подстановки Лисков и интерфейс

Нарушает ли ICollection<T>.Add()-реализация массивов принцип подстановки Лискова? Метод приводит к NotSupportedException, что нарушает LSP, ИМХО.

string[] data = new string[] {"a"};
ICollection<string> dataCollection = data;
dataCollection.Add("b");

Это приводит к

Необработанное исключение: System.NotSupportedException: коллекция имела фиксированный размер.

Я нашел довольно похожий вопрос, касающийся Stream-реализаций. Я открываю отдельный вопрос, потому что этот случай совсем другой: принцип подстановки Лисков и потоки . Разница здесь в том, что ICollection не предоставляет CanAdd-свойство или что-то подобное, как это делает Stream-класс.


person Patrik    schedule 18.05.2017    source источник
comment
Типы не могут нарушать LSP сами по себе, это только подтипы, которые не могут быть заменены своими супертипами. ICollection<T>.Add уродлив, но документирует, что NotSupportedException будет выброшено, если IsReadOnly ложно.   -  person Lee    schedule 18.05.2017


Ответы (2)


Я понимаю, почему ты так думаешь. Есть функция, которая ожидает коллекцию и ожидает, что ее можно изменить. Передача массива приведет к сбою, поэтому очевидно, что вы не можете заменить интерфейс этой конкретной реализацией, верно?

Это проблема? Может быть. Это зависит от того, как часто вы ожидаете, что идеалы сохранятся. Собираетесь ли вы случайно использовать массив вместо коллекции, а затем через десять лет удивитесь, что она сломается? Не совсем. Система типов, используемая приложениями .NET, не идеальна - она ​​не говорит вам, что это конкретное ICollection<T> использование требует, чтобы коллекция была модифицируемой.

Было бы лучше для .NET, если бы массивы не претендовали на реализацию ICollection<T> (или IEnumerable<T>, которые они также «на самом деле» не реализуют)? Я так не думаю. Есть ли способ сохранить удобство того, что массивы «будут» ICollection<T>, а также избежать того же нарушения LSP? Неа. Базовый массив по-прежнему будет иметь фиксированную длину - в лучшем случае вместо этого вы нарушите более полезные принципы (например, тот факт, что ссылочные типы не должны иметь ссылочную прозрачность).

Но ждать! Давайте посмотрим на настоящий контракт ICollection<T>.Add. Позволяет ли это NotSupportedException быть брошенным? Ах да, цитируя MSDN:

[NotSupportedException возникает, если...] Коллекция ICollection доступна только для чтения.

И массивы действительно возвращают true, когда вы запрашиваете IsReadOnly. Контракт сохраняется.

Если вы считаете, что Stream не нарушает LSP из-за CanWrite, вы должны считать массивы допустимыми коллекциями, поскольку они имеют IsReadOnly, а это true. Если функция принимает доступную только для чтения коллекцию и пытается добавить в нее, это ошибка функции. Невозможно указать это явно в C#/.NET, поэтому вам нужно полагаться на другие части контракта, а не только на типы, например. в документации к функции должно быть указано, что NotSupportedException (или ArgumentException, или что-то еще) выдается для коллекции, доступной только для чтения. Хорошая реализация сделает этот тест прямо в начале функции.

Важно отметить, что типы в C# не так ограничены, как в теории типов, где определена LSP. Например, вы можете написать такую ​​функцию на C#:

bool IsFrob(object bobicator)
{
  return ((Bob)bobicator).IsFrob;
}

Можно ли заменить bobicator любым супертипом object? Явно нет. Но точно так же это не проблема бедного типа Frobinate — это ошибка в функции IsFrob. На практике большая часть кода на C# (и большинстве других языков) работает только с объектами, гораздо более ограниченными, чем это может быть указано типом в сигнатуре метода.

Объект нарушает LSP только в том случае, если он нарушает контракт своего супертипа. Он не может нести ответственность за другие нарушения кодаg LSP. И часто вы обнаружите, что довольно прагматично создавать код, который идеально не работает в LSP — инженерия всегда была связана с компромиссами. Тщательно взвесьте расходы.

person Luaan    schedule 18.05.2017

Нет, поскольку это не класс - отношения между интерфейсом и реализующим классом не совпадают с отношениями между суперклассом и подклассом.

LSP конкретно применяется к поведению кода, которое подразумевает реализацию — интерфейс не имеет реализации, поэтому LSP не применяется.

Однако это является нарушением принципа разделения интерфейсов, который гласит, что вам следует скорее создавать интерфейсы, чем избегать нереализованных методов.

person toadflakz    schedule 18.05.2017
comment
Это правда, что LSP был определен в контексте создания подтипов; однако я не совсем уверен, является ли это действительным аргументом. Плохо, когда подтип не может заменить супертип, и хорошо, когда реализация интерфейса не может заменить интерфейс? Вы правы, это нарушение ISP, но в C#/.NET это невозможно решить — вы не можете взять существующий интерфейс и разделить его на несколько интерфейсов. - person Luaan; 18.05.2017
comment
@DarrenYoung - вы бы этого не сделали «по определению», потому что ICollection<T>.Add допустимо только в том случае, если IsReadOnly возвращает false, а массивы возвращают true. - person Lee; 18.05.2017
comment
@Luaan: интерфейсы (контракты методов) не определяют поведение, это определяет только реализация, а реализация может быть обеспечена только классом. Следовательно, LSP не может применяться, потому что этот принцип конкретно применим к поведению. - person toadflakz; 18.05.2017
comment
Да, в строгом смысле теории типов. Но программирование — это нечто большее, чем просто то, что определяется системой типов — интерфейсы теоретически не имеют поведения, но их контракты (например, документация) могут описывать допустимое и недопустимое поведение интерфейса. В контракте интерфейса есть нечто большее, чем просто имена и типы методов. Вот почему я не проголосовал за ваш ответ или не проголосовал за него - я, честно говоря, не могу решить, стоит ли рассматривать теорию, когда вопрос касается исключительно кодирования, где все эти вещи гораздо менее четкие, чем в теории. - person Luaan; 18.05.2017
comment
Скорее, интерфейс ICollection<T> — это плохо разработанный код от Microsoft, потому что он неправильно применяет принципы — дебаты существуют только потому, что этот конкретный интерфейс является частью фреймворка. В любом другом месте это назвали бы плохой реализацией и отметили бы изменение. Я не согласен с прагматизмом, но с базовыми принципами вы не идете на компромисс, если можете изменить код. - person toadflakz; 18.05.2017