Антипаттерны и запахи

Принцип разделения интерфейса концептуально похож на принцип единственной ответственности и решает схожие проблемы, только относящиеся к более высокому уровню абстракции.

Два самых распространённых «запаха», намекающих, что нарушен 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. Более навороченный инженерный калькулятор смог бы реализовать несколько интерфейсов одновременно с помощью множественного наследования.

Материалы к разделу

Вопросы