Скажем, у нас есть список сущностей задач и ProjectTask
подтип. Задачи могут быть закрыты в любое время, кроме тех, ProjectTasks
которые не могут быть закрыты, если они имеют статус Запущено. Пользовательский интерфейс должен гарантировать, что опция закрытия запуска ProjectTask
никогда не будет доступна, но в домене присутствуют некоторые меры безопасности:
public class Task
{
public Status Status { get; set; }
public virtual void Close()
{
Status = Status.Closed;
}
}
public class ProjectTask : Task
{
public override void Close()
{
if (Status == Status.Started)
throw new Exception("Cannot close a started Project Task");
base.Close();
}
}
Теперь при вызове Close()
Задачи существует вероятность, что вызов не удастся, если он имеет ProjectTask
статус «Запущено», а если нет, то если это была базовая Задача. Но это бизнес-требования. Это должно потерпеть неудачу. Можно ли считать это нарушением принципа подстановки Лискова ?
design
object-oriented-design
solid
liskov-substitution
Пол Т Дэвис
источник
источник
public Status Status { get; private set; }
:; в противном случаеClose()
метод можно обойти.Task
не вносят причудливых несовместимостей в полиморфный код, о котором только известно,Task
- это большая проблема. LSP не прихоть, но был введен именно для того, чтобы облегчить обслуживание в больших системах.TaskCloser
процесс, которыйclosesAllTasks(tasks)
. Этот процесс явно не пытается перехватить исключения; в конце концов, это не часть явного договораTask.Close()
. Теперь вы вводитеProjectTask
и неожиданноTaskCloser
начинаете генерировать (возможно необработанные) исключения. Это большое дело!Ответы:
Да, это нарушение LSP. Принцип замещения Лискова требует, чтобы
Ваш пример нарушает первое требование, усиливая предварительное условие для вызова
Close()
метода.Это можно исправить, перенеся усиленное предварительное условие на верхний уровень иерархии наследования:
Предусматривая, что вызов
Close()
действителен только в состоянии, приCanClose()
возврате которогоtrue
вы делаете предварительное условие, применимое как к,Task
так и кProjectTask
исправлению нарушения LSP:источник
Close
проверка на верхнем уровне и добавление защищеннойDoClose
будет допустимой альтернативой. Однако я хотел остаться как можно ближе к примеру ОП; улучшение это отдельный вопрос.Да. Это нарушает LSP.
Я предлагаю добавить
CanClose
метод / свойство к базовой задаче, чтобы любая задача могла определить, можно ли закрыть задачу в этом состоянии. Это также может дать причину. И удалить виртуальный изClose
.Основываясь на моем комментарии:
источник
Принцип подстановки Лискова гласит, что базовый класс должен заменяться любым из его подклассов без изменения каких-либо желательных свойств программы. Поскольку
ProjectTask
исключение возникает только при закрытии, программа должна быть заменена на acommodate для этого, должнаProjectTask
использоваться вместоTask
. Так что это нарушение.Но если вы измените,
Task
указав в своей подписи, что оно может вызвать исключение при закрытии, вы не нарушите этот принцип.источник
Нарушение LSP требует трех сторон. Тип T, Подтип S и программа P, которая использует T, но получает экземпляр S.
Ваш вопрос предоставил T (Task) и S (ProjectTask), но не P. Таким образом, ваш вопрос является неполным и ответ квалифицирован: если существует P, который не ожидает исключения, тогда для этого P у вас есть LSP нарушение. Если каждый P ожидает исключения, то нарушения LSP нет.
Тем не менее, вы делаете иметь SRP нарушение. Тот факт, что состояние задачи может быть изменено, и политика, согласно которой определенные задачи в определенных штатах не должны быть изменены на другие состояния, являются двумя совершенно разными обязанностями.
Эти две обязанности меняются по разным причинам и поэтому должны быть в разных классах. Задачи должны обрабатывать факт выполнения задачи и данные, связанные с задачей. TaskStatePolicy должен обрабатывать способ перехода задач из состояния в состояние в данном приложении.
источник
OpenTaskException
(подсказку, подсказку), и каждый P ожидает исключения, то что это говорит о коде для интерфейса, а не о реализации? О чем я говорю? Я не знаю. Я просто в восторге от того, что комментирую ответ Unca 'Bob.Это может или не может быть нарушением LSP.
Шутки в сторону. Выслушайте меня.
Если вы следуете LSP, объекты типа
ProjectTask
должны вести себя так, какTask
ожидают себя объекты типа .Проблема с вашим кодом в том, что вы не задокументировали, как
Task
должны вести себя объекты типа . Вы написали код, но без контрактов. Я добавлю контракт наTask.Close
. В зависимости от контракта, который я добавляю, кодProjectTask.Close
либо соответствует, либо не соответствует LSP.Учитывая следующий контракт для Task.Close, код для
ProjectTask.Close
не соответствует LSP:Учитывая следующий контракт для Task.Close, код для
ProjectTask.Close
действительно следует за LSP:Методы, которые могут быть переопределены, должны быть задокументированы двумя способами:
«Поведение» документирует, на что может положиться клиент, который знает, что объект-получатель является
Task
, но не знает, к какому классу он относится напрямую. Он также сообщает дизайнерам подклассов, какие переопределения являются разумными, а какие нет.«Поведение по умолчанию» документирует, на что может положиться клиент, который знает, что объект-получатель является прямым экземпляром
Task
(т.е. что вы получаете, если вы используетеnew Task()
. Он также сообщает дизайнерам подклассов, какое поведение будет унаследовано, если они этого не сделают переопределить метод.Теперь должны соблюдаться следующие отношения:
источник
Close
действительно выбрасывает. Таким образом, подпись заявляет, что может быть выдано исключение, но не говорит, что этого не произойдет. Java делает лучшую работу в этом отношении. Тем не менее, если вы объявляете, что метод может объявлять исключение, вы должны задокументировать обстоятельства, при которых он может (или будет). Поэтому я утверждаю, что для того, чтобы быть уверенным в том, что LSP нарушен, нам нужна документация за пределами подписи.if (false) throw new Exception("cannot start")
базового класса. Компилятор удалит это, и все же код содержит то, что необходимо. Btw. у нас все еще есть нарушение LSP с этими обходными путями, потому что предварительное условие все еще усилено ...Это не нарушение принципа подстановки Лискова.
Принцип замещения Лискова гласит:
Причина, по которой ваша реализация подтипа не является нарушением принципа подстановки Лискова, довольно проста: ничто не может быть доказано о том, что на
Task::Close()
самом деле делает. Конечно,ProjectTask::Close()
выдает исключение, когдаStatus == Status.Started
, но может иStatus = Status.Closed
вTask::Close()
.источник
Да, это нарушение.
Я бы предложил, чтобы у вас была иерархия в обратном направлении. Если не каждый
Task
закрывается, тоclose()
не принадлежитTask
. Возможно, вы хотите интерфейс,CloseableTask
который все неProjectTasks
могут реализовать.источник
Task
сам не реализует,CloseableTask
то они делают небезопасный бросок куда-то, чтобы даже позвонитьClose()
.В дополнение к проблеме LSP, кажется, что он использует исключения для управления потоком программы (я должен предположить, что вы где-то ловите это тривиальное исключение и делаете некоторый пользовательский поток, а не позволяете ему аварийно завершить работу приложения).
Похоже, что это хорошее место для реализации шаблона State для TaskState и позволяющего объектам состояния управлять допустимыми переходами.
источник
Здесь я упускаю важную вещь, связанную с LSP и проектированием по контракту - в предварительных условиях именно вызывающий абонент должен убедиться, что предварительные условия выполнены. В теории DbC вызываемый код не должен проверять предварительное условие. В контракте должно быть указано, когда задача может быть закрыта (например, CanClose возвращает True), а затем вызывающий код должен убедиться, что предварительное условие выполнено, прежде чем вызывать метод Close ().
источник
ProjectTask
. Это постусловие (оно говорит, что происходит после вызова метода), и его выполнение является обязанностью вызываемого кода.Да, это явное нарушение LSP.
Некоторые люди утверждают, что в базовом классе указание на то, что подклассы могут генерировать исключения, сделало бы это приемлемым, но я не думаю, что это правда. Независимо от того, что вы документируете в базовом классе или на какой уровень абстракции вы перемещаете код, предварительные условия все равно будут усилены в подклассе, поскольку вы добавляете в него часть «Не удается закрыть запущенную задачу проекта». Это не то, что вы можете решить с помощью обходного пути, вам нужна другая модель, которая не нарушает LSP (или нам нужно ослабить ограничение «предварительные условия не могут быть усилены»).
Вы можете попробовать шаблон декоратора, если вы хотите избежать нарушения LSP в этом случае. Это может сработать, я не знаю.
источник