В реальной жизни

ISP можно рассматривать как принцип единой ответственности (SRP) для интерфейсов. Его главная задача — помочь спроектировать интерфейсы так, чтобы в них не было ничего лишнего. Принцип помогает выявлять более высокие абстракции и находить неочевидные связи между сущностями.

Траты и доходы в Койне

Приложение Койн — это приложение для экономии, будущий преемник Тяжеловато и площадка для экспериментов.

В Койне есть возможность записывать траты и доходы. Разработчики использовали ISP, чтобы сделать работу с этими двумя сущностями более удобной.

Список трат и доходов в Койне

Сперва в приложении были только траты, поэтому в истории хранились только объекты типа Spend.

interface Spend {
  amount: number
  date: Timestamp
  type: 'helpful' | 'harmful' | null
}

type History = Spend[]

Когда в приложении добавились доходы, в истории стали храниться не только траты. У трат и доходов были общие поля, поэтому было логично вынести их в общий интерфейс Record, а интерфейс траты расширить от него:

type RecordTypes = 'spend' | 'income'
type SpendTypes = 'helpful' | 'harmful' | null

// Общие поля описаны в общем интерфейсе.
// ISP помогает выявить скрытые связи
// между сущностями и зависимости в их поведении:

interface Record {
  amount: number
  date: Timestamp
  is: RecordTypes

  // Поля `type` здесь уже нет,
  // для доходов оно не нужно.
}

// Интерфейс траты расширяет интерфейс записи:

interface Spend extends Record {
  type: SpendTypes
}

Траты и доходы стали реализовать эти интерфейсы так, чтобы базовая запись описывала общее поведение, а трата и доход — только специфичное для них:

class RecordItem implements Record {
  amount: number
  date: Timestamp
  is: RecordTypes

  constructor(amount: number) {
    this.amount = amount
    this.date = Date.now()
  }
}

class SpendItem extends RecordItem implements Spend {
  type: SpendTypes

  constructor(amount: number, type: SpendTypes = null) {
    super(amount)
    this.is = 'spend'
    this.type = type
  }
}

class IncomeItem extends RecordItem {
  constructor(amount: number) {
    super(amount)
    this.is = 'income'
  }
}

Так функциональность не дублируется, но классы при этом не зависят от методов, которые им не нужны.

Уведомления в Задачнике

Задачник — это система управления задачами и учёта времени работы.

Для напоминания о подходящих сроках задачи в Задачнике используются уведомления. Уведомления бывают разных типов: push, SMS и почтовые.

Согласно ISP, общие для всех типов поля и методы разработчики хранят в общем интерфейсе Message:

interface Message {
  title: string
  body: string
  send(to: string[]): void
}

Детали же — описывают конкретно под каждый тип уведомлений:

interface SmsMessage extends Message {
  smsService: SmsService
  // ...
}

interface PushMessage extends Message {
  pushService: PushService
  // ...
}

interface EmailMessage extends Message {
  emailService: EmailService
  // ...
}

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