Pull to refresh

Осторожно, истинные контракты классов могут отличаться от формальных

Reading time3 min
Views9.3K
Вкратце, в этой статье речь пойдёт о правиле наследования Лисков, о различии контрактов NotifyCollectionChangedAction.Reset в версиях .NET Framework 4 и .NET Framework 4.5, и о том, какой из этих двух контрактов истинный, а какой — ошибочный.



Согласно принципу Лисков, класс-наследник должен наследовать контракт базового класса (с возможностью добавления своей специфики, не противоречащей исходному контракту).

Приведу пример. Представьте, что метод Add List-а виртуальный. Если вы создаёте наследника от List<>, то метод Add в нём должен добавлять в коллекцию ровно один элемент. Если элемент будет добавляться только при выполнении некоторого условия, или же будут добавляться элемент и его копия, то пользовательский код, который ожидает, что после вызова Add Count увеличивается ровно на единицу, станет неработоспособным. Поведение наследуемых классов должно быть ожидаемым для кода, использующего переменную базового типа.

Теперь давайте представим, что вы собрались использовать в своём коде List<>. Судя по названию Add и параметрам (один элемент), метод должен добавить один элемент в коллекцию. Вы много раз пользовались листом, и уверены, что так оно и есть. Вы можете спросить у коллеги, и он не задумываясь подтвердит, что так оно и есть. Но давайте на минуту представим, что вы заходите на msdn, смотрите документацию, а там написано, что Add просто «изменяет исходную коллекцию», т.е. делает что угодно. В таком случае будем называть тот контракт, который характерен для базового класса, и на который все полагаются, истинным, а тот, который описан в документации — формальным.

Если, создавая наследника класса, вы будете полагаться на формальное поведение, а не на истинное, то формально вы ничего не нарушите, но фактически создадите бомбу замедленного действия, которая когда-нибудь может создать для кого-нибудь (скорее всего, не для вас) проблемы.

Пример расхождения формального и истинного контракта — NotifyCollectionChangedAction.Reset. До версии 4.5 Reset означал, что содержимое коллекции сильно изменилось. Что значит «сильно»? Для кого-то добавление трёх элементов сильное изменение, а для кого-то и нет.

В общем, Reset означал «коллекция изменилась как угодно». Начиная с версии 4.5 Reset стал означать очистку коллекции. Некоторые могут подумать, что это изменение было внесено напрасно, т.к. оно нарушает обратную совместимость, но я скажу, что ребята молодцы — они вовремя заметили, что истинный контракт расходится с формальным, и оперативно исправили свою оплошность. Используя ObservableCollection, можно встретить Reset, только если у объекта был вызван метод Clear(). Программисты, регулярно работающие с ObservableCollection, привыкли к этому и считают это нормой. «Когда может встретиться Reset?», — спросите вы их, и они, не задумываясь, ответят: «Когда был вызван Clear!». Естественно, они интуитивно считают, что это поведение, де-факто ставшее стандартом, должно сохраняться и в наследниках. Поэтому в документации должно быть сказано, что Reset — признак очистки коллекции.

Подводя итог: в случае, если вы реализуете наследника, опирайтесь на наиболее конкретный и специфичный контракт среди формального и истинного. Если вы используете класс, опирайтесь на наименее специфичный контракт среди формального и истинного.

Используя Reset, считайте, что он может означать что угодно. Наследуя ObservableCollection, считайте, что Reset означает очистку коллекции.

P.S. Если вам интересно моё мнение по поводу Reset, я считаю, что разработчикам класса ObservableCollection следует оставить контракт Reset-а в том виде, в котором он есть на сегодняшний день (признак очистки коллекции), но добавить в перечисление элемент, сигнализирующий о том, что коллекция изменилась как угодно, и который не использовался бы в оригинальном ObservableCollection. Дело в том, что единственный элемент перечисления, сигнализирующий о том, что изменилось несколько элементов коллекции — это Reset, остальные элементы перечисления сигнализируют об изменении единичного элемента. Однажды, для достижения приемлемого быстродействия, одному программисту потребовалось сначала изменить несколько элементов в коллекции, и потом послать ровно один сигнал об изменении коллекции. И у него не осталось другого выбора, кроме как сигнализировать об изменении коллекции в своём наследнике от ObservableCollection посредством Reset-а, за неимением других альтернатив.

Так что я считаю, что изменение документации решило одну проблему, но одновременно создало другую (для решения которой нужно добавить ещё один элемент в перечисление). Забавно, но иногда неиспользуемые зарезервированные элементы способны принести пользу.
Tags:
Hubs:
+8
Comments16

Articles