Правило 7±2

Написание кода составляет лишь 20% времени в работе программиста. Большую часть своего рабочего времени он тратит на чтение кода.

Будучи программистом мне приходилось читать много кода. И зачастую этот код был плохо читаем. Надеюсь, вам повезло больше, и вам попадались проекты с «чистым» кодом и понятной структурой, по лучшим заветам Uncle Bob и Мартина Фаулера, но так бывает далеко не всегда. Особенно, в области frontend-разработки, развитие которой началось с появлением AJAX. Впервые инфраструктура AJAX была упомянута 18 февраля 2005 года в статье Джесси Джеймса Гарретта «Новый подход к веб-приложениям».

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

Человек не может одновременно запомнить и повторить более 7 объектов. Эта закономерность называется «кошелёк Миллера». По последним исследованием это число оказывается да же завышено и скорее для большинства людей равно 5. Таким образом, когда мы читаем код, в котором содержится больше 7 объектов (переменных, вызовов функций), нам становиться все сложнее удержать их в голове, что вызывает когнитивные сложности. Ниже приведен пример такого кода. Это простой HTTP-сервер, написанный на NodeJS.

const server = http.createServer((req, res) => {
  const { url } = req

  if (url === '/favicon.ico') {
    const filePath = path.join(process.cwd(), 'static', url)
    
    const stream = fs.createReadStream(filePath)

    if (!stream) {
      res.statusCode = 404
      res.end('Not found')
      return
    }

    const headers = { 'Content-Type': 'image/x-icon' }
    
    res.writeHead(200, headers)

    stream.pipe(res)
    
    res.on('close', () => stream.destroy())
  } else if (url === '/') {
    const { pipe, abort } = renderToPipeableStream(
      <App>My super app…</App>, {
        bootstrapScripts: ['/main.js'],
        onShellReady () {
          res.setHeader('Content-Type', 'text/html; charset=UTF-8')
          pipe(res)
        },
        onShellError () {
          res.statusCode = 500
          res.setHeader('Content-Type', 'text/html; charset=UTF-8')
          res.end('Something went wrong :(')
        },
      })

    res.on('close', abort)
  } else {
    res.statusCode = 404
    res.end('Not found')
    return
  }
})

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

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

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

Попробуем применить правило 7±2.

const server = http.createServer(function server(req, res) {
  const { url } = req

  if (url === '/favicon.ico') {
    sendStaticFile(res, url)
    return
  } else if (url === '/') {
    sendSSR(res)
    return
  } else {
    sendNotFound(res)
    return
  }
})

function sendStaticFile(res, name) {
  const filePath = path.join(process.cwd(), 'static', name)

  const stream = fs.createReadStream(filePath)
  
  if (!stream) {
    sendNotFound(res)
    return
  }

  const headers = { 'Content-Type': 'image/x-icon' }

  res.writeHead(200, headers)

  stream.pipe(res)

  res.on('close', () => stream.destroy())
}

function sendSSR(res) {
  const { pipe, abort } = renderToPipeableStream(
    <App>My super app…</App>, {
      bootstrapScripts: ['/main.js'],
      onShellReady () {
        res.setHeader('Content-Type', 'text/html; charset=UTF-8')
        pipe(res)
      },
      onShellError () {
        res.statusCode = 500
        res.setHeader('Content-Type', 'text/html; charset=UTF-8')
        res.end('Something went wrong :(')
      },
    })

  res.on('close', abort)
}

function sendNotFound() {
  res.statusCode = 404
  res.end('Not found')
}

Вместо одной функции, теперь код состоит из четырех небольших функций: server, sendStaticFile, sendSSR и sendNotFound. Каждая функция содержит не больше 7 объектов (вызовов). Например, server состоит из req, res, url, sendStaticFile(), sendSSR() и sendNotFound(). Теперь при беглом взгляде на функцию server становится понятно, что она делает. Данный код еще далек от идеала, и не следует некоторым принципам программирования, но уже стал намного явнее.

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

Правило 7±2, заставляет «держать» на одном уровне кода (функции, метода, модуля) не больше 7±2 объектов, что не вызывает сложности для восприятия. Если требуется «углубиться» в детали, то мы переходим на следующий уровень, где количество объектов так же не превышает 7±2. Например, требуется узнать детали реализации отправки статичных файлов сервера: изучив server() переходим в sendStaticFile() и так далее. Можно привести аналогию - зум. Изначально код представлен в общих чертах, зумируя его определенную часть, раскрывается все больше деталей.

Данный подход можно найти в посте Марка Симанна о «фрактальной архитектуре». Марк представляет программы, как набор вложенных друг в друга шестиугольников. В посте есть наглядное представление данного подхода.

Рекомендую придерживаться правила 7±2 не только на одном уровне, но и вглубь. Мне встречались 10-15 вложенные вызовы функций. Пока зумишь код вглубь, забываешь с чего начал. Изучать код такого проекта, ненамного лучше первого примера этого поста.

Правило 7±2 небольшими усилиями позволит сократить время на чтение кода, чем большую часть времени занят программист. Упростить навигацию по проекту. Новым сотрудникам будет легче понять проект.

Написание понятного и расширяемого кода - задача непростая и креативная. Этому посвящено немало статей и книг. Правило 7±2 поможет в решении этой задачи.