Правило 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 поможет в решении этой задачи.