Теория вместо эвристики: становимся лучше как frontend-разработчики

Автор оригинала: Alexander Kotler
  • Перевод


Перевод Becoming a better front-end developer using fundamentals instead of heuristics

Наш опыт показывает, что не имеющие технического образования разработчики и самоучки чаще полагаются не на теоретические принципы, а на эвристические методы.

Эвристика — шаблоны и проверенные правила, которые разработчик вынес из практики. Они могут работать неидеально или ограниченно, но в достаточной мере, и не требуют серьёзных размышлений. Вот некоторые примеры эвристики:

  • «Используй $(document).ready(function(){}) для инициализации кода на jQuery-сайтах»
  • «Конструкция var self = this необходима для вызова метода в функции обратного вызова»
  • «У стрелочных функций нет операторов return»

В то же время, теоретический принцип может быть использован для поиска решений других проблем. Он неизменно верен и часто определяет само устройство работы того или иного элемента. К теоретическим принципам относятся, например:


Обратите внимание: мы заключили в кавычки только примеры эвристики — для того, чтобы подчеркнуть кустарный характер эвристики по сравнению со строгостью теоретических основ. Ни один из примеров эвристики не является универсальным для всех случаев, но они работают в достаточном количестве ситуаций, чтобы применяющие их разработчики получали рабочий код без полного понимания его работы.

Аргументы в пользу теоретического подхода


Мы часто сталкивались с тем, что не имеющие технического образования разработчики не склонны решать проблемы с помощью теоретических принципов. Как правило, это объясняется тем, что в начале карьеры у них не было возможности их выучить, и, поскольку эвристические методы работают удовлетворительно, они продолжают ими пользоваться.

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

К тому же, полагаясь на эвристические методы, вы никогда не научитесь решать проблемы по-настоящему. Возможно, достаточно часто вам удастся находить рабочее решение, но рано или поздно вы зайдете в тупик, выхода из которого не увидите. На эвристику в своей работе полагаются 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 новый нестандартный метод.

Но Биллу всё равно — работает ведь! Последствия его пока не интересуют.

Что Билл сделал не так?


Билл попробовал эвристический метод решения проблемы и столкнулся со следующими проблемами:

  1. Использовав .filter() на переменной, Билл получил TypeError. Он не понимал, что filter не определяется на обычных объектах.
  2. Он применил Object.keys(), чтобы «превратить объект в массив», но само по себе это результата не принесло. Ему нужно было создать массив значений объекта.
  3. Даже получив значения и использовав их как условие для фильтрации, он получил всего лишь идентификаторы вместо пользовательских объектов, ассоциирующихся с этими идентификаторами. Всё потому, что фильтруемый массив содержал ID, а не пользовательские объекты.
  4. Со временем Билл отказался от этого подхода и нашёл рабочее решение в интернете. Тем не менее, он до сих пор не понял, как оно работает — и не станет тратить время на то, чтобы разобраться, ведь у него есть и другие дела.

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

Перейдем к основам


Если бы Билл был сторонником теоретического подхода, то процесс выглядел бы так:

  1. Идентифицировать заданные входные данные и определить желаемые выходные — в смысле их свойств: «У меня есть объект, ключами которого являются строки, представляющие ID, а значениями — объекты, представляющие пользователей. Я хочу получить массив, значениями которого будут пользовательские объекты — но только объекты зарегистрированных пользователей»
  2. Понять, как произвести перебор внутри объекта: «Я знаю, что могу получить массив ключей в объекте, вызвав Object.keys(). Я хочу получить массив потому, что массивы поддерживают перебор».
  3. Осознать, что этот метод помогает получить ключи, а вам нужно трансформировать ключи в значения, и вспомнить про map — очевидный метод создания нового массива путём трансформирования значений другого массива:

    Object.keys(usersById).map(id => usersById[id])
  4. Увидеть, что теперь у вас есть массив пользовательских объектов, который можно фильтровать и который содержит действительные значения, которые вы хотите отфильтровать:

    Object.keys(usersById).map(id => usersById[id]).filter(user => user.registered)

Пойди Билл этим путем, он мог бы работать у нас.

Почему люди не прибегают к теории чаще?


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

Чтобы избежать подобных ошибок, всегда начинайте с теории. На каждой стадии процесса задумывайтесь, с какими именно данными вы имеете дело. Вместо того, чтобы всё время полагаться на знакомые шаблоны, учитывайте примитивные типы данных: массив, объект, строка и т.д. При использовании функции или метода обращайтесь к документации, чтобы точно знать, какие типы данных их поддерживают, какие они принимают аргументы и какой получается результат.

С таким подходом вы сможете находить рабочее решение с первой попытки. Вы можете быть уверены в его правильности, ведь вы специально подбирали свои действия, основываясь на заданных входных и желаемых выходных данных. Вникайте в основы каждой операции (типы данных и возвращаемые значения), а не размытые бизнес-формулировки (вроде «зарегистрированных пользователей»).
NIX
79,63
Компания
Поделиться публикацией

Комментарии 21

    +2
    Object.keys(usersById).map(id => usersById[id]).filter(user => user.registered)

    Вообще-то, довольно странный путь.
    Во-первых, в функцию map попадает что-то из внешней области видимости, (что впоследствии может привести к излишнему каррированию).
    А во-вторых, почему бы не сэкономить лишний пробег, воспользовавшись Object.values или же Object.entries?
    А в-третьих, функция, принимающая на входе объект-коллекцию, почему-то выдает массив, не то, что будет ожидать следующий разработчик.


    Object.fromEntries(Object.entries(usersById).filter(([id, user]) => user.registered))
    // Object.fromEntries можно заменить на функцию-помощник, написанную через reduce
      0

      Методы fromEntries и entries ещё не вошли в стандарт. Через reduce надёжней:


       const registredUsersById = Object.values(usersById).reduce((result, user) => {
              if(!user.registered) return result
              return {
                  ...result,
                  [user.id]: user
              }
          }, {})
        +1
        `Object.entries` уже в стандарте ES2017,
          0
          Зачем столько кода? Вот же самый короткий и рабочий путь.
          const registeredUsers = Object.values(usersById).filter(user => user.registered);
          
            0

            Сравните результаты:


            .reduce()
            {
              "5": {
                "id": "5",
                "name": "Adam",
                "registered": true
              },
              "27": {
                "id": "27",
                "name": "Bobby",
                "registered": true
              },
              "39": {
                "id": "39",
                "name": "Danielle",
                "registered": true
              }
            }

            .filter()
            [
              {
                "id": "5",
                "name": "Adam",
                "registered": true
              },
              {
                "id": "27",
                "name": "Bobby",
                "registered": true
              },
              {
                "id": "39",
                "name": "Danielle",
                "registered": true
              }
            ]

            reduce — возвращает объект, а filterмассив

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

              Допустим, Биллу нужно извлечь список зарегистрированных пользователей.

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

              Object.keys(usersById).map(id => usersById[id]).filter(user => user.registered)
              

              Пойди Билл этим путем, он мог бы работать у нас.
                0

                Постановка задачи не включала информацию о формате выходных данных

                  0
                  Ну как же нет? В переводе рассматривается абстрактный пример на собеседовании некоего Билла. Ниже цитата из перевода, от этого условия скачет дальнейший код, в этом условии просят извлечь список. Более того, результат в виде массива логичен и может быть обоснован дальнейшим использованием, то есть если мы получим отфильтрованный объект, то для отрисовки данных нам снова придется приводить его к массиву.
                  Допустим, Биллу нужно извлечь список зарегистрированных пользователей.
                    0
                    Если бы

                    Размышления о том, что на выходе он мог бы получить массив, находятся в секции после постановки задачи.


                    Вся задача укладывается в одно предложение:


                    Биллу нужно извлечь список зарегистрированных пользователей.

                    Но про Object.values() вы правы, так проще.

                      0
                      Тут да, спорить не буду, хотя в каком-то смысле это можно отнести к придиркам. Но оставим это на совести автора.
                      Размышления о том, что на выходе он мог бы получить массив, находятся в секции после постановки задачи.
              0

              del

          –1

          Функциональщина это, конечно, круто, но почему бы просто не использовать for in?
          Минус два прохода по всем элементам массива


          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 }
          }
          const result = [];
          for(let key in usersById) {
              if(usersById[key].registered) {
                result.push(usersById[key]);
              }
          }
            –1

            И однажды случайно получить "пользователей" с идентификаторами toString и пр.?


            Тогда уж надо цикл for-of использовать...

              0
              А можно узнать про это поподробнее? Или где можно почитать про это?
              Просто сама только начала учить JavaScript и мне важно понять как делать правильно.
          +5
          Особенно плакать хочется, когда на все проблемы в ангуляре, люди закидывают в ответы вредные советы вида:
          1. Поставь setTimeout(() => {}, 0) должно помочь! Если не поможет, то поменяй 0 на 100;
          2. Если падает "...expression has changed after it was checked...", то советуют в случайных местах повтыкать cdr;
          3. Если не понимают как работает DI, То советуют ВСЁ вытащить в рут-модуль, либо наоборот продублировать во всех чалдовых.

          Список можно продолжать очень долго, первое что мгновенно всплыло. А уж какие советы дают «по борьбе с rxjs» совсем песня… количество семафоров, флажков и прочих подпорок поражает воображение.
            +3
            Вы сравниваете не эвристику с теорией, а неопытного разработчика с опытным.
              +2
              Почему не использовать метод values?
              Object.values(usersById).filter(user => user.registered)

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

              Самое читаемое