Держите обещание
Принцип подстановки Лисков гласит: везде, где код ожидает интерфейс, каждая реализация этого интерфейса должна работать правильно. Реализация, которая формально подходит, но ведёт себя плохо — паникует, ничего не делает, обманывает — ломает любую функцию, доверившуюся интерфейсу.
Интерфейс — это обещание. Fly() обещает полёт. Тип, который реализует Fly() через аварийное завершение, подписал договор, который не может выполнить.
Замена, которая нарушает договор
Интерфейс Bird обещает Fly(). Воробьи это обещание выполняют. Пингвины тоже птицы — поэтому кажется естественным реализовать интерфейс... через панику, ведь пингвины не умеют летать.
Теперь airShow, которая работает с любой честной птицей, аварийно завершается на пингвине. Ошибка не в airShow — она в подстановке, нарушившей обещание.
Пингвин, который срывает шоу
airShow доверяет интерфейсу и вызывает Fly() у каждой птицы. Воробей выступает; пингвин паникует и обрушивает всю программу (recover лишь смягчает падение, чтобы показать сообщение).
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
type Bird interface {
Fly() string
}
type Sparrow struct{}
func (s Sparrow) Fly() string {
return "воробей: летит низко"
}
// Penguin подписывает договор — но не может его выполнить.
type Penguin struct{}
func (p Penguin) Fly() string {
panic("пингвины не умеют летать")
}
func airShow(birds []Bird) {
for _, b := range birds {
fmt.Println(b.Fly())
}
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("шоу сорвалось:", r)
}
}()
airShow([]Bird{Sparrow{}, Penguin{}})
}
Разделите обещания
Решение: обещать только то, что правда. Разделите «толстое» обещание на Flyer и Swimmer. Воробей реализует Flyer, пингвин реализует Swimmer — и ни один тип не подписывает договор, который не может выполнить.
Самое приятное — кто теперь ловит ошибку: передача пингвина в airShow больше не аварийное завершение во время выполнения, а ошибка компиляции. Программа просто не соберётся.
Каждый тип держит своё обещание
airShow принимает Flyer, poolShow принимает Swimmer. Каждое животное появляется только там, где его обещание выполнимо, — подстановка безопасна по построению.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
type Flyer interface {
Fly() string
}
type Swimmer interface {
Swim() string
}
type Sparrow struct{}
func (s Sparrow) Fly() string {
return "воробей: летит низко"
}
type Penguin struct{}
func (p Penguin) Swim() string {
return "пингвин: ныряет глубоко"
}
func airShow(flyers []Flyer) {
for _, f := range flyers {
fmt.Println(f.Fly())
}
}
func poolShow(swimmers []Swimmer) {
for _, s := range swimmers {
fmt.Println(s.Swim())
}
}
func main() {
airShow([]Flyer{Sparrow{}})
poolShow([]Swimmer{Penguin{}})
// airShow([]Flyer{Penguin{}}) даже не скомпилируется.
}
Любой Flyer может присоединиться
Выгода честных интерфейсов: новый тип Eagle присоединяется к авиашоу без всякого риска — всё, что реализует Flyer, действительно летает, поэтому airShow никогда не приходится сомневаться в своих входных данных.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
type Flyer interface {
Fly() string
}
type Sparrow struct{}
func (s Sparrow) Fly() string {
return "воробей: летит низко"
}
// Eagle — новый тип, и он действительно летает, поэтому подставляется безопасно.
type Eagle struct{}
func (e Eagle) Fly() string {
return "орёл: парит высоко"
}
func airShow(flyers []Flyer) {
for _, f := range flyers {
fmt.Println(f.Fly())
}
}
func main() {
airShow([]Flyer{Sparrow{}, Eagle{}})
}