Страницы

Поиск по вопросам

суббота, 13 июля 2019 г.

Принцип подстановки Лисков и предусловия

Принцип подстановки Лисков прямо подразумевает, что предусловия не должны усиливаться в подклассах. Это логично (потому что переопределённый метод подкласса, для которого входные данные окажутся неприемлимыми, будет работать некорректно/выбросит эксепшн). Но как тогда спроектировать следующую архитектуру:
Есть класс скажем Vehicle (можно его сделать абстрактным, не суть важно), у него есть виртуальный метод Weight (пусть этот метод делает какие-то заумные вычисления для каждого типа транспортного средства в зависимости от массы, не столь важно):
public abstract class Vehicle { public virtual void Weight(int w) {
} }
Теперь у нас есть класс Motorcycle, который мы наследуем от Vehicle, но мы должны ограничить его макс массу скажем в 200 кг.
public class Motorcycle : Vehicle { public override void Weight(int w) { if (w > 200) throw new WeightOverflowException(); } }
вроде всё прекрасно, логичное наследование, с наследованием всех свойств и поведения, но нарушает же LSP:
List list2 = new List(); list2.Add(new Motorcycle());
foreach (var item in list2) { item.Weight(400); }
полетит же эксепшн WeightOverflowException, о котором базовый класс не должен вообще ничего знать. Как правильно спроектировать такую задачу?


Ответ

В проектировании по контракту существует понятие "доступности предусловия". Это означает, что клиентский код должен иметь возможность узнать, а удовлетворяет ли он предусловия или нет.
Тут можно сказать, что поскольку в предусловии участвует аргумент, который клиент и передает, то это правило соблюдается. Но само предусловие меняется в зависимости от типа и не является явным.
В этом случае мы можем выделить предусловие в отдельный метод и сделать само предусловие полиморфным:
public abstract class Vehicle { public virtual bool IsWeightValid(int w) {return true;} public virtual void Weight(int w) { Contract.Requires(IsWeightValid(w)); } }
Теперь, мы говорим нашим клиентам: "прежде чем дергать метод Weight убедитесь самостоятельно, что аргумент валиден". Да, это может выглядеть перебором. И я бы прибегал к этому трюку в крайнем случае, чтобы не усложнять API. Но это довольно распространенный паттерн. Например, коллекции в BCL выставляют свойство IsReadOnly, которое (теоретически) должно быть проверено перед вызовом метода Add.
Кто-то считает поведение из BCL нарушением LSP, но я так не думаю: метод имеет право выражать свои предусловия в более абстрактном виде, через свои собственные свойства или методы. Contract.Requires(ImInValidState) ничем не хуже (теоретически), чем Contract.Requires(methodArgument != null)

Комментариев нет:

Отправить комментарий