Маленькие интерфейсы
Принцип разделения интерфейсов гласит: ни один тип не должен быть вынужден реализовывать методы, которые ему не нужны. Вместо одного «толстого» интерфейса со множеством методов создайте несколько маленьких — каждый тип реализует только то, что для него действительно верно.
В TypeScript маленькие интерфейсы обходятся дёшево: типы соответствуют интерфейсу по форме, поэтому класс удовлетворяет интерфейсу с одним методом просто потому, что у него есть этот метод. Вы можете описать крошечный интерфейс ровно под то, что нужно вашей функции, и каждое подходящее значение уже будет ему соответствовать.
Толстый интерфейс
Интерфейс Worker с методами work() и eat() выглядит безобидно — пока на работу не устраивается робот. Робот прекрасно работает, но он обязан реализовать и eat(), причём... хоть как-нибудь. Бессмысленные ответы вроде этого — верный признак толстого интерфейса.
Робот, вынужденный есть
Робот реализует eat() только потому, что этого требует интерфейс. Обеденный перерыв выводит ответ, которого не должно существовать вовсе — класс был вынужден солгать.
Вывод: программа на TypeScript и захваченный вывод
// Один толстый интерфейс — каждый работник ОБЯЗАН ещё и есть.
interface Worker {
work(): string
eat(): string
}
class Human implements Worker {
work(): string {
return "человек: работает"
}
eat(): string {
return "человек: обедает"
}
}
class Robot implements Worker {
work(): string {
return "робот: работает"
}
// Навязано интерфейсом — роботы не едят.
eat(): string {
return "робот: ОШИБКА, не может есть"
}
}
const team: Worker[] = [new Human(), new Robot()]
for (const w of team) {
console.log(w.work())
}
for (const w of team) {
console.log(w.eat())
}
Разделите интерфейс
Разделите толстый интерфейс на Worker и Eater. Человек реализует оба; робот реализует только Worker. Рабочая смена принимает работников, обеденный перерыв — едоков, и робота просто никогда не зовут на обед, а ни один метод не лжёт.
Каждый тип реализует только свою правду
Два маленьких интерфейса вместо одного толстого. У робота больше вообще нет метода eat() — средство проверки типов автоматически не пускает его на обеденный перерыв.
Вывод: программа на TypeScript и захваченный вывод
// Два маленьких интерфейса — каждый класс реализует только свою правду.
interface Worker {
work(): string
}
interface Eater {
eat(): string
}
class Human implements Worker, Eater {
work(): string {
return "человек: работает"
}
eat(): string {
return "человек: обедает"
}
}
class Robot implements Worker {
work(): string {
return "робот: работает"
}
}
function workShift(team: Worker[]): void {
for (const w of team) {
console.log(w.work())
}
}
function lunchBreak(guests: Eater[]): void {
for (const e of guests) {
console.log(e.eat())
}
}
workShift([new Human(), new Robot()])
lunchBreak([new Human()])
Маленькие части складываются
Маленькие интерфейсы — это не ограничение: они комбинируются. Когда какой-то функции действительно нужны обе способности, она составляет из них один интерфейс прямо на месте: Worker плюс Eater.
Именно к этому и стоит стремиться: описывайте маленькие интерфейсы и позвольте тому, кому нужно больше, склеить их с помощью extends.
Составляйте, когда нужны обе способности
WorkingEater построен из двух маленьких интерфейсов. Полному рабочему дню нужны обе способности, поэтому подходит только человек — и сказано это один раз, в системе типов.
Вывод: программа на TypeScript и захваченный вывод
interface Worker {
work(): string
}
interface Eater {
eat(): string
}
// Маленькие интерфейсы при необходимости складываются в более крупные.
interface WorkingEater extends Worker, Eater {}
class Human implements WorkingEater {
work(): string {
return "человек: работает"
}
eat(): string {
return "человек: обедает"
}
}
function fullDay(we: WorkingEater): void {
console.log(we.work())
console.log(we.eat())
console.log(we.work())
}
fullDay(new Human())