Маленькие интерфейсы
Принцип разделения интерфейсов гласит: ни один тип не должен быть вынужден реализовывать методы, которые ему не нужны. Вместо одного «толстого» интерфейса с множеством методов сделайте несколько маленьких — и каждый тип реализует только то, что верно для него.
Из пяти принципов этот ближе всего к духу Go: стандартная библиотека Go построена из крошечных интерфейсов, самые известные из которых — интерфейсы с одним методом для чтения и записи, к которым подключается половина экосистемы.
«Толстый» интерфейс
Интерфейс Worker с методами Work() и Eat() выглядит безобидно — пока на работу не устроится робот. Робот прекрасно работает, но он обязан реализовать и Eat() — хоть чем-нибудь. Бессмысленные ответы такого рода — верный признак «толстого» интерфейса.
Робот, вынужденный есть
Робот реализует Eat() только потому, что этого требует интерфейс. Обеденный перерыв печатает ответ, которого вообще не должно существовать: тип заставили солгать.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
// Один «толстый» интерфейс — каждый работник ОБЯЗАН ещё и есть.
type Worker interface {
Work() string
Eat() string
}
type Human struct{}
func (h Human) Work() string { return "человек: работает" }
func (h Human) Eat() string { return "человек: обедает" }
type Robot struct{}
func (r Robot) Work() string { return "робот: работает" }
// Навязано интерфейсом — роботы не едят.
func (r Robot) Eat() string { return "робот: ОШИБКА, не может есть" }
func main() {
team := []Worker{Human{}, Robot{}}
for _, w := range team {
fmt.Println(w.Work())
}
for _, w := range team {
fmt.Println(w.Eat())
}
}
Разделите интерфейс
Разделите «толстый» интерфейс на Worker и Eater. Человек реализует оба, робот — только Worker. Рабочая смена принимает работников, обеденный перерыв — тех, кто ест: робота просто никогда не зовут на обед, и ни один метод не лжёт.
Каждый тип реализует только свою правду
Два маленьких интерфейса вместо одного «толстого». У робота вообще больше нет метода Eat() — компилятор автоматически не пускает его на обеденный перерыв.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
// Два маленьких интерфейса — каждый тип реализует только то, что верно для него.
type Worker interface {
Work() string
}
type Eater interface {
Eat() string
}
type Human struct{}
func (h Human) Work() string { return "человек: работает" }
func (h Human) Eat() string { return "человек: обедает" }
type Robot struct{}
func (r Robot) Work() string { return "робот: работает" }
func workShift(team []Worker) {
for _, w := range team {
fmt.Println(w.Work())
}
}
func lunchBreak(guests []Eater) {
for _, e := range guests {
fmt.Println(e.Eat())
}
}
func main() {
workShift([]Worker{Human{}, Robot{}})
lunchBreak([]Eater{Human{}})
}
Маленькие части складываются
Маленькие интерфейсы — это не ограничение: они комбинируются. Когда какой-то функции действительно нужны обе способности, она прямо на месте складывает их в один интерфейс: Worker плюс Eater.
Именно так устроена стандартная библиотека: определяйте интерфейсы с одним методом, а тот, кому нужно больше, пусть склеит их вместе.
Составляйте, когда нужны обе способности
WorkingEater собран из двух маленьких интерфейсов. Полному рабочему дню нужны обе способности, поэтому подходит только человек — и сказано это один раз, в системе типов.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
type Worker interface {
Work() string
}
type Eater interface {
Eat() string
}
// Маленькие интерфейсы при необходимости складываются в большие.
type WorkingEater interface {
Worker
Eater
}
type Human struct{}
func (h Human) Work() string { return "человек: работает" }
func (h Human) Eat() string { return "человек: обедает" }
func fullDay(we WorkingEater) {
fmt.Println(we.Work())
fmt.Println(we.Eat())
fmt.Println(we.Work())
}
func main() {
fullDay(Human{})
}