Принцип разделения ответственностей

В прошлой статье «Правило 7±2» говорилось о важности разделения кода на небольшие функции, что помогает в написании понятного и расширяемого кода.

После разделения кода на небольшие функции, каждая из них начинает отвечать за определенную ответственность (concern).

В статье используется термин «функция» для единообразия, но может использоваться и термин «объект», если в проекте применяется ООП.

Concern – ответственность, функциональность.

Разделение кода на различные ответственности – это базовый принцип, без которого не получиться разработать понятную архитектуру приложения. Данный принцип получил название – принцип разделения ответственностей (separation of concerns, SoC).

«Разделение ответственности – программа должны состоять из функциональных блоков, как можно меньше дублирующих функциональность друг друга» – Э. Дейкстра.

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

<template id="list">
  <ul class="list"></ul>
</template>

<template id="item">
  <li class="item"></li>
</template>

<script>
  async function main() {
    // 1. Получаем данные от сервера
    let hotels
    try {
      const hotelsReq = await fetch('/hotels')

      hotels = await hotelsReq.json()
    } catch (error) {}
     
    if (!hotels) {
      return
    }

    // 2. Выполняем преобразования данных
    const fiveStarHotels = hotels.filter(({ stars }) => stars === 5)

    // 3. Отображаем данные пользователю
    const list = document.body.querySelector('#list')
    const listClone = list.content.cloneNode(true)

    const item = document.body.querySelector('#item')

    const elems = fiveStarHotels.reduce((acc, { title }) => {
      const itemClone = item.content.cloneNode(true)
      itemClone.querySelector('.item').textContent = title
     
      acc.append(itemClone)

      return acc
    }, listClone.querySelector('.list'))
     
    document.body.append(elems)
  }

  main()
</script>

Вся функциональность находится в одной функции main(). Этот код можно разбить на три функциональных блока (ответственности):

  1. Получение данных.
  2. Преобразование данных.
  3. Отображение данных.

Вышеперечисленные ответственности есть в большинстве клиентских приложений.

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

<template id="list">
  <ul class="list"></ul>
</template>

<template id="item">
  <li class="item"></li>
</template>

<script>
  // ДОСТУП К ДАННЫМ
  async function fetchHotels() {
    try {
      const hotelsReq = await fetch('/hotels')

      return await hotelsReq.json()
    } catch (error) {}
  }

  // БИЗНЕС-ЛОГИКА
  async function main() {
    const hotels = await fetchHotels()
    if (!hotels) {
      return
    }

    const fiveStarHotels = getFiveStarHotels(hotels)

    showHotels(fiveStarHotels)
  }

  function getFiveStarHotels(hotels) {
    return hotels.filter(({ stars }) => stars === 5)
  }
   
  // ПРЕДСТАВЛЕНИЕ
  function showHotels(fiveStarHotels) {
    const elems = showList(fiveStarHotels)
   
    document.body.append(elems)
  }
   
  function showList(items) {
    const list = document.body.querySelector('#list')
    const listClone = list.content.cloneNode(true)
   
    const item = document.body.querySelector('#item')
   
    const elems = items.reduce((acc, { title }) => {
      const itemClone = item.content.cloneNode(true)
      itemClone.querySelector('.item').textContent = title
   
      acc.append(itemClone)
   
      return acc
    }, listClone.querySelector('.list'))
   
    return elems
  }
   
  main()
</script>

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

  1. Доступ к данным. Функция fetchHotels().
  2. Бизнес-логика. Функции getFiveStarHotels() и main().

    В getFiveStarHotels() происходит преобразование данных, что является бизнес-логикой. Функция main() содержит основной код приложения, какую ценность оно несёт клиентам.

  3. Представление. Функции showHotels() и showList().

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

Опытные разработчики могли заметить сходство с 3-tier архитектурой, которая разделяет код программы на три архитектурных слоя: доступ к данным, бизнес-логика и представление.

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

Сложно представить понятную архитектуру без отчетливых ответственностей и связей между ними. SoC является базовым принципом при разработке ПО и проектировании архитектуры приложения. Не рекомендуется забывать про него. Это поможет писать надежный и расширяемый код.