Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Перевод Becoming a better front-end developer using fundamentals instead of heuristics
Наш опыт показывает, что не имеющие технического образования разработчики и самоучки чаще полагаются не на теоретические принципы, а на эвристические методы.
Эвристика — шаблоны и проверенные правила, которые разработчик вынес из практики. Они могут работать неидеально или ограниченно, но в достаточной мере, и не требуют серьёзных размышлений. Вот некоторые примеры эвристики:
- «Используй
$(document).ready(function(){})
для инициализации кода на jQuery-сайтах» - «Конструкция
var self = this
необходима для вызова метода в функции обратного вызова» - «У стрелочных функций нет операторов
return
»
В то же время, теоретический принцип может быть использован для поиска решений других проблем. Он неизменно верен и часто определяет само устройство работы того или иного элемента. К теоретическим принципам относятся, например:
- Официальный синтаксис стрелочных функций
- API-документация к Lodash
- Правила применения и функции vertical-align
Обратите внимание: мы заключили в кавычки только примеры эвристики — для того, чтобы подчеркнуть кустарный характер эвристики по сравнению со строгостью теоретических основ. Ни один из примеров эвристики не является универсальным для всех случаев, но они работают в достаточном количестве ситуаций, чтобы применяющие их разработчики получали рабочий код без полного понимания его работы.
Аргументы в пользу теоретического подхода
Мы часто сталкивались с тем, что не имеющие технического образования разработчики не склонны решать проблемы с помощью теоретических принципов. Как правило, это объясняется тем, что в начале карьеры у них не было возможности их выучить, и, поскольку эвристические методы работают удовлетворительно, они продолжают ими пользоваться.
Однако, несмотря на кажущуюся сложность, выучить теорию может быть очень полезно. Зачем? Затем, что теория позволит вам чувствовать уверенность в том, что ваше решение работает, а также самостоятельно выводить ответы на новые вопросы, не имея потребности искать чужие решения. В краткосрочной перспективе эвристические алгоритмы могут казаться простым и быстрым решением, но часто будут приводить к неидеальным решениям — если вообще приводить.
К тому же, полагаясь на эвристические методы, вы никогда не научитесь решать проблемы по-настоящему. Возможно, достаточно часто вам удастся находить рабочее решение, но рано или поздно вы зайдете в тупик, выхода из которого не увидите. На эвристику в своей работе полагаются C&P-программисты.
Критерий уровня навыков разработчика
Проводя собеседование с frontend-разработчиками, мы ставим перед ними задачу по программированию и говорю, что они вольны использовать любые источники, будь то Google или Stack Overflow. Таким образом можно легко определить, является ли разработчик адептом эвристики или теории.
Первые без всякого исключения копируют код из более-менее подходящих примеров со Stack Overflow. Лишь когда код заработает не так, как планировалось, они начнут подстраивать его под себя. Часто им это не удается.
Вторые же склонны искать ответы в API-документации. Там они находят информацию о том, сколько каких параметров принимает та или иная функция, или конкретный синтаксис развернутой формы нужного CSS-свойства.
Уже в первые пять минут собеседования можно точно определить, к какому типу программистов относится кандидат.
Пример
Возьмем для примера разработчика Билла. Он прошел несколько обучающих курсов, решил некоторое количество задач на JavaScript и в свободное время писал сайты, но «по-настоящему» JavaScript не изучал.
Однажды Биллу попадается объект наподобие этого:
const usersById = {
"5": { "id": "5", "name": "Adam", "registered": true },
"27": { "id": "27", "name": "Bobby", "registered": true },
"32": { "id": "32", "name": "Clarence", "registered": false },
"39": { "id": "39", "name": "Danielle", "registered": true },
"42": { "id": "42", "name": "Ekaterina": "registered": false }
}
Такой объект может отображать список пользователей и то, зарегистрировались ли они на определенное мероприятие.
Допустим, Биллу нужно извлечь список зарегистрированных пользователей. Иными словами, отфильтровать их. Ему попадался код, в котором метод
.filter()
использовался для фильтрации списка. Поэтому он пробует что-то вроде:const attendees = usersById.filter(user => user.registered);
И вот что он получает:
TypeError: usersById.filter is not a function
«Какая-то бессмыслица», — думает Билл, ведь он видел код, в котором
.filter()
срабатывал в качестве фильтра.Проблема в том, что Билл положился на эвристический метод. Он не понимает, что
filter
— метод, определяемый на массивах, тогда как usersById
— обычный объект, не имеющий метода filter
.Сбитый с толку Билл гуглит «javascript фильтр». Он находит множество упоминаний массивов и понимает, что ему нужно превратить
usersById
в массив. Затем по запросу «javascript превратить объект в массив» он находит на Stack Overflow примеры с использованием Object.keys()
. После этого он пробует:const attendees = Object.keys(usersById).filter(user => user.registered);
На этот раз ошибка не выводится, но, к удивлению Билла, поле
attendees
остается пустым.Дело в том, что
Object.keys()
возвращает ключи объекта, но не его значения. По сути, наименование переменной user
легко вводит в заблуждение, поскольку это не объект user
, а идентификатор, то есть строка. Так как атрибут registered
для строк не определен, filter
расценивает каждую запись как ложную, и массив выходит пустым.Билл присматривается к ответам на Stack Overflow поближе и вносит следующее изменение:
const attendees = Object.keys(usersById).filter(id => usersById[id].registered);
На этот раз результат лучше:
["5", "27", "39"]
. Но Билл хотел получить объекты посетителей, а не их ID.Чтобы понять, как отфильтровать посетителей, раздраженный Билл ищет «javascript фильтр объектов», изучает результаты поиска по Stack Overflow и находит этот ответ со следующим кодом:
Object.filter = (obj, predicate) =>
Object.keys(obj)
.filter( key => predicate(obj[key]) )
.reduce( (res, key) => (res[key] = obj[key], res), {} );
Билл копирует эти строки и пробует:
const attendees = Object.filter(usersById, user => user.registered);
Всё работает — хотя и непонятно, почему. Билл не понимает, для чего нужен
reduce
и как он используется. Более того, Билл не понимает, что всего лишь определил для глобального объекта Object
новый нестандартный метод.Но Биллу всё равно — работает ведь! Последствия его пока не интересуют.
Что Билл сделал не так?
Билл попробовал эвристический метод решения проблемы и столкнулся со следующими проблемами:
- Использовав
.filter()
на переменной, Билл получилTypeError
. Он не понимал, чтоfilter
не определяется на обычных объектах.
- Он применил
Object.keys()
, чтобы «превратить объект в массив», но само по себе это результата не принесло. Ему нужно было создать массив значений объекта. - Даже получив значения и использовав их как условие для фильтрации, он получил всего лишь идентификаторы вместо пользовательских объектов, ассоциирующихся с этими идентификаторами. Всё потому, что фильтруемый массив содержал ID, а не пользовательские объекты.
- Со временем Билл отказался от этого подхода и нашёл рабочее решение в интернете. Тем не менее, он до сих пор не понял, как оно работает — и не станет тратить время на то, чтобы разобраться, ведь у него есть и другие дела.
Это искусственный пример, но мы много раз сталкивались с разработчиками, решающими проблемы в той же манере. Чтобы решать их эффективно, нужно отойти от эвристических методов и изучить теорию.
Перейдем к основам
Если бы Билл был сторонником теоретического подхода, то процесс выглядел бы так:
- Идентифицировать заданные входные данные и определить желаемые выходные — в смысле их свойств: «У меня есть объект, ключами которого являются строки, представляющие ID, а значениями — объекты, представляющие пользователей. Я хочу получить массив, значениями которого будут пользовательские объекты — но только объекты зарегистрированных пользователей»
- Понять, как произвести перебор внутри объекта: «Я знаю, что могу получить массив ключей в объекте, вызвав
Object.keys()
. Я хочу получить массив потому, что массивы поддерживают перебор». - Осознать, что этот метод помогает получить ключи, а вам нужно трансформировать ключи в значения, и вспомнить про
map
— очевидный метод создания нового массива путём трансформирования значений другого массива:
Object.keys(usersById).map(id => usersById[id])
- Увидеть, что теперь у вас есть массив пользовательских объектов, который можно фильтровать и который содержит действительные значения, которые вы хотите отфильтровать:
Object.keys(usersById).map(id => usersById[id]).filter(user => user.registered)
Пойди Билл этим путем, он мог бы работать у нас.
Почему люди не прибегают к теории чаще?
Иногда они просто с ней не знакомы. Чаще всего они слишком заняты и не могут найти время, чтобы изучить этот способ решения проблем — им просто нужно, чтобы всё работало. Они рискуют превратить этот подход в привычку, которая станет препятствием для развития их навыков.
Чтобы избежать подобных ошибок, всегда начинайте с теории. На каждой стадии процесса задумывайтесь, с какими именно данными вы имеете дело. Вместо того, чтобы всё время полагаться на знакомые шаблоны, учитывайте примитивные типы данных: массив, объект, строка и т.д. При использовании функции или метода обращайтесь к документации, чтобы точно знать, какие типы данных их поддерживают, какие они принимают аргументы и какой получается результат.
С таким подходом вы сможете находить рабочее решение с первой попытки. Вы можете быть уверены в его правильности, ведь вы специально подбирали свои действия, основываясь на заданных входных и желаемых выходных данных. Вникайте в основы каждой операции (типы данных и возвращаемые значения), а не размытые бизнес-формулировки (вроде «зарегистрированных пользователей»).