Принцип подстановки Лисков прямо подразумевает, что предусловия не должны усиливаться в подклассах. Это логично (потому что переопределённый метод подкласса, для которого входные данные окажутся неприемлимыми, будет работать некорректно/выбросит эксепшн). Но как тогда спроектировать следующую архитектуру:
Есть класс скажем 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
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)
Комментариев нет:
Отправить комментарий