Шаблоны проектирования и приёмы рефакторинга

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

Адаптер

Адаптер — шаблон проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе.

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

Приложение Курсовик показывает курс доллара к рублю. Оно берёт данные курсов с сайта Центрального банка России. ЦБ отдаёт их в формате XML, а Курсовик работает с JSON.

Адаптер помогает «подружить» модули, которые работают с XML, и модули, которые работают с JSON.

const ApiClient = {
  async getXml(url: string): Promise<XmlString> {
    const response = await fetch(url)
    const data = await response.text()
    return data
  }
}

const xmlJsonAdapter = (xml: XmlString): JsonString => {
  // ...конвертируем xml в json
  return json
}

const parseCourse = (data: JsonString): CourseDict => {
  // ...
  return course
}

(async () => {
  const data = await ApiClient.getXml('api_endpoint')
  const course = parseCourse(xmlJsonAdapter(data))
})()

Также адаптер помогает справляться со «сломанным» API от бекенда и преобразованием одних структур данных в другие.

Из минусов можно назвать:

  • добавление ещё одной абстракции в кодовую базу проекта;
  • при создании нового адаптера нужно найти все места, где требуется его использовать.

Выделение интерфейса

Выделение интерфейса — это приём, при котором одинаковые методы и поля выносят в отдельный интерфейс.

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

Выделение интерфейса тесно связано не только с ISP, но и с LSP. Например, оно используется при поиске корня композиции и как вспомогательный инструмент для выделения суперкласса.

Множественное наследование

Множественное наследование используется, например, чтобы реализовать функциональность нескольких интерфейсов:

class Horse implements Animal, Transport {/*...*/}

В TypeScript такое наследование реализуется через миксины.

До этих пор в книге для простоты повествования мы пропускали специальную функцию applyMixins, которая копирует функциональность из родительских классов:

function applyMixins(derivedCtor: any, baseCtors: any[]) {
  baseCtors.forEach(baseCtor => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
      Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name))
    })
  })
}

Чтобы пример с классом Horse выше сработал, нам необходимо использовать applyMixins следующим образом:

applyMixins(Horse, [Animal, Transport])

Тогда множественное наследование будет работать, как мы ожидаем.

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

Вопросы