Принцип разделения интерфейса гласит:

Клиенты не должны зависеть от методов, которые они не используют.

Простыми словами: лучше создать много маленьких, специализированных интерфейсов, чем один огромный и универсальный («толстый»). Когда интерфейс слишком большой, классы, которые его реализуют, вынуждены писать пустые методы-заглушки для функций, которые им на самом деле не нужны.


❌ Нарушение принципа (плохой код)

Представьте, что мы создаем интерфейс для умных устройств SmartDevice. У нас есть навороченный принтер (который умеет всё) и старый черно-белый принтер (который умеет только печатать).

// Огромный "толстый" интерфейс
interface SmartDevice {
  print(): void;
  scan(): void;
  fax(): void;
}

// Навороченный офисный принтер — ему всё подходит
class AllInOnePrinter implements SmartDevice {
  public print() { console.log("Печать документа..."); }
  public scan() { console.log("Сканирование..."); }
  public fax() { console.log("Отправка факса..."); }
}

// Простой принтер на складе — нарушает ISP
class OldPrinter implements SmartDevice {
  public print() {
    console.log("Печать документа...");
  }

  // Нарушение ISP: класс вынужден реализовывать методы, которые он не поддерживает!
  public scan() {
    throw new Error("Я старый принтер, я не умею сканировать!");
  }

  public fax() {
    throw new Error("У меня нет факса!");
  }
}

Почему это плохо? Класс OldPrinter зависит от функций scan и fax, которых у него физически нет. Если мы случайно вызовем эти методы в коде, программа упадет с ошибкой.


✅ Соблюдение принципа (хороший код)

Мы разделяем один большой интерфейс на три маленьких и независимых. Теперь каждый класс берет (реализует) только то, что ему действительно нужно для работы.

1. Создаем маленькие специализированные интерфейсы

interface Printer {
  print(): void;
}

interface Scanner {
  scan(): void;
}

interface Fax {
  fax(): void;
}

2. Реализуем классы без лишнего кода

Благодаря TypeScript, один класс может реализовывать сразу несколько интерфейсов через запятую.

// Старый принтер берет только интерфейс Printer
class OldPrinter implements Printer {
  public print() {
    console.log("Печать документа на старом принтере...");
  }
}

// Современный комбайн реализует ВСЕ интерфейсы разом
class AllInOnePrinter implements Printer, Scanner, Fax {
  public print() { console.log("Печать..."); }
  public scan() { console.log("Сканирование..."); }
  public fax() { console.log("Отправка факса..."); }
}

3. Безопасное использование

Теперь функции, работающие с устройствами, точно знают, на что способен объект.

// Функция принимает только то устройство, которое ГАРАНТИРОВАННО умеет сканировать
function processDocument(scanner: Scanner) {
  scanner.scan();
}

const superPrinter = new AllInOnePrinter();
const oldPrinter = new OldPrinter();

processDocument(superPrinter); // Всё отлично работает!
// processDocument(oldPrinter);  // Ошибка TypeScript! Мы даже не сможем скомпилировать этот код, защитив себя от багов в рантайме.

В чем выгода ISP?

  1. Безопасность типов: Вы никогда не вызовете несуществующий метод у объекта, так как TypeScript подсветит ошибку еще на этапе написания кода.
  2. Гибкость: Код становится модульным. Классы не перегружены логикой «на всякий случай».
  3. Чистота: Отсутствуют пустые методы-заглушки со строками return null или throw new Error().