Антипаттерны и запахи
Неправильная или неполная реализация некоторых шаблонов проектирования или приёмов, а также неправильная иерархия сущностей могут нарушить LSP.
Непредсказуемое изменение поведения
Допустим, мы делаем клон Медиума, где авторы будут публиковать статьи. Статья может находиться в разных состояниях, от которых зависит, что с ней можно делать. Например, удалённую статью нельзя удалить, а опубликованную — снова опубликовать.
Для подобной задачи подходит шаблон Состояние — он позволяет менять поведение объектов в зависимости от их внутреннего состояния. Если он реализован правильно и полно, то LSP он не нарушит. Однако реализация в примере ниже нарушает.
Допустим, статья описывается базовым классом Article
:
enum ArticleStatus {
Draft
Published
Deleted
}
class Article {
status: ArticleStatus
constructor() {/*...*/}
edit() {/*...*/}
delete() {/*...*/}
restore() {/*...*/}
unpublish() {/*...*/}
publish(): void {
this.status = ArticleStatus.Published
}
}
Если опубликованная статья при попытке публикации выбрасывает исключение, которое не было описано в базовом классе, то мы нарушаем LSP:
class Published extends Article {
constructor() {
super({ status: ArticleStatus.Published })
}
publish(): void {
// Упс!
throw new Error('article is already published')
}
}
Чтобы реализация шаблона не нарушала LSP, нам необходимо описать в базовом классе возможность выбросить исключение. Для этого мы введём метод, который будет определять, можно ли статью публиковать:
class ArticleException extends Error {/*...*/}
class Article {
// ...
protected canPublish(): boolean {
return this.status === ArticleStatus.Draft
}
publish(): void {
if (!this.canPublish()) throw new ArticleException()
// ...
}
}
Сейчас переопределение метода publish
для опубликованной статьи не будет усиливать предусловия, поэтому это не нарушит LSP:
class ArticlePublishedException extends ArticleException {/*...*/}
class Published extends Article {
// ...
publish(): void {
// `ArticlePublishedException` наследуется от `ArticleException`,
// указанного в классе `Article`, поэтому здесь нарушения нет:
throw new ArticlePublishedException()
}
}
Вопросы
Интерфейс, которому нельзя доверять
Более тонкое нарушение LSP — это «пустая» реализация интерфейса.
Если опереться на пример выше, то пустой реализацией было бы описание метода Publish
для опубликованной статьи таким образом:
class Published extends Article {
// ...
publish(): void {
return
}
}
Вроде всё хорошо: метод описывает правильное поведение, усиления предусловия нет. Но если посмотреть на ситуацию в терминах контрактного программирования, то метод publish
должен менять состояние статьи с Draft
на Published
, чего не будет происходить:
class Article {
// ...
publish(): void {
// Проверяем, что состояние позволяет публиковать статью
// именно эта проверка в `Published` вызовет ошибку:
this.contract.require(this.canPublish() === true)
// ...
// Проверяем, что состояние поменялось на `Published`:
this.contract.ensure(this.status === ArticleStatus.Published)
}
}
«Пустая» реализация интерфейса также нарушает принцип разделения интерфейса.