Антипаттерны и запахи
Принцип разделения интерфейса концептуально похож на принцип единственной ответственности и решает схожие проблемы, только относящиеся к более высокому уровню абстракции.
Два самых распространённых «запаха», намекающих, что нарушен ISP, — это грязный интерфейс и «пустая» реализация.
Грязный интерфейс
Проблема грязного интерфейса возникает, когда интерфейс содержит в себе слишком много методов и полей. Как и большие модули в SRP, грязный интерфейс в ISP приводит к плохой читаемости и дорогой поддержке.
Рассмотрим проблему на примере. Допустим, у нас есть калькулятор, который умеет складывать, вычитать, умножать, делить и брать квадратные корни.
interface Calculator {
add(a: number, b: number): number
subtract(a: number, b: number): number
multiply(a: number, b: number): number
divide(a: number, b: number): number
sqrt(a: number): number
}
class BasicCalculator implements Calculator {
// ...
}
При расширении функциональности интерфейс может стать слишком большим:
interface Calculator {
add(a: number, b: number): number
subtract(a: number, b: number): number
multiply(a: number, b: number): number
divide(a: number, b: number): number
sqrt(a: number): number
power(base: number, power: number): number
sin(x: number): number
cos(x: number): number
tan(x: number): number
log(x: number): number
log10(x: number): number
// ...
}
Класс, реализующий такой интерфейс будет неоправданно большим, из-за чего сложность понимания кода резко вырастет.
Кроме высокой сложности грязные интерфейсы приводят и ещё к одной проблеме, которую мы упоминали в разделе с примерами из идеального мира — пустой реализации.
«Пустая» реализация
«Пустая» реализация интерфейса возникает, когда появляется модуль, которому не нужны все методы из реализуемого интерфейса.
Попробуем создать арифметический калькулятор. Ему не нужны тригонометрические функции и логарифмы, но из-за того, что они описаны в интерфейсе, нам придётся поставить заглушки на эти методы:
class ArithmeticCalculator implements Calculator {
// функциональность методов, которые нужны, описываем полностью
add(a: number, b: number): number {
return a + b
}
subtract(a: number, b: number): number {
return a - b
}
multiply(a: number, b: number): number {
return a * b
}
divide(a: number, b: number): number {
if (b === 0) {/*...*/}
return a / b
}
// остальные методы не нужны;
// но интерфейс Calculator требует их реализации,
// приходится писать заглушки
sqrt(a: number): number
power(base: number, power: number): number
sin(x: number): number { return 0 }
cos(x: number): number { return 0 }
tan(x: number): number { return 0 }
log(x: number): number { return 0 }
log10(x: number): number { return 0 }
// ...
}
ISP решает
ISP помогает проектировать интерфейсы, избегая проблем выше. На примере калькулятора мы бы могли выделить несколько интерфейсов с соответствующими ролями:
interface ArithmeticCalculator {
add(a: number, b: number): number
subtract(a: number, b: number): number
multiply(a: number, b: number): number
divide(a: number, b: number): number
}
interface ExponentCalculator {
sqrt(a: number): number
power(base: number, power: number): number
}
interface TrigonometricCalculator {
sin(x: number): number
cos(x: number): number
tan(x: number): number
}
interface LogarithmicCalculator {
log(x: number): number
log10(x: number): number
}
Тогда арифметическому калькулятору достаточно было бы реализовать интерфейс ArithmeticCalculator
. Более навороченный инженерный калькулятор смог бы реализовать несколько интерфейсов одновременно с помощью множественного наследования.