В идеальном мире

В идеальном мире каждый класс в коде решает одну и только одну задачу, а все задачи структурированы и разделены. Модули в этом случае дополняют друг друга, а их совокупность детально описывает систему.

Допустим, у нас есть задача создать отчёт об активности пользователей и вывести его в нескольких вариантах: как строку HTML или TXT.

Отчёт

Мы создадим класс ReportExporter, который будет заниматься только экспортом данных. Определять необходимый формат будет класс FormatSelector. А форматированием данных будут заниматься классы: HtmlFormatter и TxtFormatter.

// Тип данных для отчёта:

type ReportData = {
  content: string,
  date: Date,
  size: number,
}

// Возможные форматы:

enum ReportTypes {
  Html,
  Txt,
}

// Класс, который занимается экспортом данных:

class ReportExporter {
  name: string
  data: ReportData

  constructor(name: string, data: ReportData) {
    this.name = name
    this.data = data
  }

  export(reportType: ReportTypes): string {
    const formatter: Formatter = FormatSelector.selectFor(reportType)
    return formatter.format(this.data)
  }
}

Форматы экспорта

В соответствии с SRP форматирование данных — это отдельная задача. Поэтому для преобразования данных отчёта в необходимый формат мы создадим отдельные классы.

interface Formatter {
  format(data: ReportData): string
}

// Класс для форматирования в HTML:
class HtmlFormatter implements Formatter {
  format(data: ReportData): string {

    // ...Форматируем данные в HTML и возвращаем:
    return 'html string'
  }
}

// Класс для форматирования в TXT:
class TxtFormatter implements Formatter {
  format(data: ReportData): string {

    // ...Форматируем данные в TXT и возвращаем:
    return 'txt string'
  }
}

Выбор формата

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

Для решения этой задачи воспользуемся шаблоном проектирования «Стратегия», который поможет выбрать подходящий формат. (Более подробно «Стратегию» мы разберём в разделе о принципе открытости и закрытости.)

Создадим новый класс FormatSelector, который будет выбирать тип форматирования, в зависимости от настроек.

class FormatSelector {
  private static formatters = {
    [ReportTypes.Html]: HtmlFormatter,
    [ReportTypes.Txt]: TxtFormatter,
  }

  static selectFor(reportType: ReportTypes) {
    const FormatterFactory = FormatSelector.formatters[reportType]
    return new FormatterFactory();
  }
}

const dynamicFormatter = FormatSelector.selectFor(ReportTypes.Html)
dynamicFormatter.format(/*...*/)

Таким образом SRP помогает разделить ответственность за различные задачи между сущностями и сделать это так, чтобы каждая сущность занималась одной задачей.

Вопросы