Софт постоянно усложняется. Стабильность и простота расширения приложения напрямую зависят от качества кода.


К сожалению, почти каждый разработчик, и я в том числе, в своей работе сталкивается с кодом плохого качества. И это — болото. У такого кода есть токсичные признаки:


  • Функции слишком длинные, и на них слишком много задач
  • Часто у функций есть побочные эффекты, которые сложно определить, а иногда даже сложно отлаживать
  • Непонятные имена у функций и переменных
  • Хрупкий код: небольшая модификация неожиданно ломает другие компоненты приложения
  • Плохое покрытие кода тестами или вообще его отсутствие

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


Однажды мой коллега уволился, потому что пытался справиться с REST API на Ruby, который было трудно поддерживать. Он получил этот проект от предыдущей команды разработчиков.


Исправление текущих ошибок создавало новые, добавление новых функций рождало новую серию ошибок, и так далее (хрупкий код). Клиент не хотел перестраивать приложение, делать ему удобную структуру, и разработчик принял правильное решение — уволиться.



Такие ситуации случаются часто, и это печально. Но что делать?


Во-первых, помнить: создать работающее приложение и позаботиться о качестве кода — разные задачи.


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


Функции (в том числе методы объекта) — это маленькие шестерёнки, которые заставляют приложение работать. В начале вы должны сосредоточиться на их структуре и составе. Статья охватывает лучшие подходы, как писать простые, понятные и легко тестируемые функции.


1. Функции должны быть маленькими. Совсем маленькими.


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


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


  • 1 балл за null или undefined
  • 2 балла за примитивный тип
  • 4 балла за объект или функцию

Например, сумма массива [null, 'Hello World', {}] вычисляется так: 1 (за null) + 2 (за строку, примитивный тип) + 4 (за объект) = 7.


Шаг 0: Первичная большая функция


Давайте начнем с худшего метода. Идея — писать код одной большой функцией getCollectionWeight():


function getCollectionWeight(collection) {  
  let collectionValues;
  if (collection instanceof Array) {
    collectionValues = collection;
  } else if (collection instanceof Map) {
    collectionValues = [...collection.values()];
  } else {
    collectionValues = Object.keys(collection).map(function (key) {
      return collection[key];
    });
  }
  return collectionValues.reduce(function(sum, item) {
    if (item == null) {
      return sum + 1;
    } 
    if (typeof item === 'object' || typeof item === 'function') {
      return sum + 4;
    }
    return sum + 2;
  }, 0);
}
let myArray = [null, { }, 15];  
let myMap = new Map([ ['functionKey', function() {}] ]);  
let myObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)  
getCollectionWeight(myMap);    // => 4  
getCollectionWeight(myObject); // => 2  

Проблема хорошо видна. Функция getCollectionWeight() слишком раздутая и выглядит как черный ящик, полный сюрпризов.


Вам, скорее всего, с первого взгляда сложно понять, какая у неё задача. А представьте набор таких функций в приложении.


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



Шаг 1: Извлекаем вес по типу и ликвидируем магические числа


Теперь цель — разбить длинную функцию на мелкие, независимые и переиспользуемые. Первый шаг — извлечь код, который определяет сумму значения по его типу. Эта новая функция будет называться getWeight().


Также обратите внимание на магические цифры этой суммы: 1, 2 и 4. Просто чтение этих цифр, без понимания всей истории, не даёт полезной информации. К счастью, ES2015 позволяет объявить const как read-only, так что можно легко создавать константы со значимыми именами и ликвидировать магические числа.


Давайте создадим небольшую функцию getWeightByType() и одновременно усовершенствуем getCollectionWeight():


// Code extracted into getWeightByType()
function getWeightByType(value) {  
  const WEIGHT_NULL_UNDEFINED  = 1;
  const WEIGHT_PRIMITIVE       = 2;
  const WEIGHT_OBJECT_FUNCTION = 4;
  if (value == null) {
    return WEIGHT_NULL_UNDEFINED;
  } 
  if (typeof value === 'object' || typeof value === 'function') {
    return WEIGHT_OBJECT_FUNCTION;
  }
  return WEIGHT_PRIMITIVE;
}
function getCollectionWeight(collection) {  
  let collectionValues;
  if (collection instanceof Array) {
    collectionValues = collection;
  } else if (collection instanceof Map) {
    collectionValues = [...collection.values()];
  } else {
    collectionValues = Object.keys(collection).map(function (key) {
      return collection[key];
    });
  }
  return collectionValues.reduce(function(sum, item) {
    return sum + getWeightByType(item);
  }, 0);
}
let myArray = [null, { }, 15];  
let myMap = new Map([ ['functionKey', function() {}] ]);  
let myObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)  
getCollectionWeight(myMap);    // => 4  
getCollectionWeight(myObject); // => 2  

Правда, выглядит лучше?


Функция getWeightByType() — независимый компонент, который просто определяет сумму по типу. И она переиспользуемая, потому что может выполняться в пределах любой другой функции.


getCollectionWeight() становится чуть более облегчённой


WEIGHT_NULL_UNDEFINED, WEIGHT_PRIMITIVE и WEIGHT_OBJECT_FUNCTION — не требующие объяснения константы, которые описывают типы сумм. Вам не нужно догадываться, что означают цифры 1, 2 и 4.


Шаг 2: Продолжаем разделение и делаем функции расширяемыми


Обновленная версия по-прежнему обладает недостатками.


Представьте себе, что у вас есть план реализовать сравнение значений Set или вообще другой произвольной коллекции. getCollectionWeight() будет быстро увеличиваться в размерах, так как её логика — собирать значения.


Давайте извлечём код, который собирает значения из map getMapValues??() и простых JavaScript-объектов getPlainObjectValues??() в отдельные функции. Посмотрите на улучшенную версию:


function getWeightByType(value) {  
  const WEIGHT_NULL_UNDEFINED = 1;
  const WEIGHT_PRIMITIVE = 2;
  const WEIGHT_OBJECT_FUNCTION = 4;
  if (value == null) {
    return WEIGHT_NULL_UNDEFINED;
  } 
  if (typeof value === 'object' || typeof value === 'function') {
    return WEIGHT_OBJECT_FUNCTION;
  }
  return WEIGHT_PRIMITIVE;
}
// Code extracted into getMapValues()
function getMapValues(map) {  
  return [...map.values()];
}
// Code extracted into getPlainObjectValues()
function getPlainObjectValues(object) {  
  return Object.keys(object).map(function (key) {
    return object[key];
  });
}
function getCollectionWeight(collection) {  
  let collectionValues;
  if (collection instanceof Array) {
    collectionValues = collection;
  } else if (collection instanceof Map) {
    collectionValues = getMapValues(collection);
  } else {
    collectionValues = getPlainObjectValues(collection);
  }
  return collectionValues.reduce(function(sum, item) {
    return sum + getWeightByType(item);
  }, 0);
}
let myArray = [null, { }, 15];  
let myMap = new Map([ ['functionKey', function() {}] ]);  
let myObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)  
getCollectionWeight(myMap);    // => 4  
getCollectionWeight(myObject); // => 2  

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


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


Шаг 3: Никогда не прекращайте улучшения


Даже на этом этапе у вас есть много возможностей для повышения качества!


Вы можете создать отдельную getCollectionValues??(), которая содержит операторы if/else и дифференцирует типы коллекций:


function getCollectionValues(collection) {  
  if (collection instanceof Array) {
    return collection;
  }
  if (collection instanceof Map) {
    return getMapValues(collection);
  }
  return getPlainObjectValues(collection);
}

Тогда getCollectionWeight() станет действительно простой, потому что единственное, что нужно сделать, это получить значения коллекции getCollectionValues?() и применить к нему sum reducer.


Можно также создать отдельную функцию сокращения:


function reduceWeightSum(sum, item) {  
  return sum + getWeightByType(item);
}

Потому что в идеале getCollectionWeight() не должна определять функции.


В конце концов начальная большая функция превращается в маленькие:


function getWeightByType(value) {  
  const WEIGHT_NULL_UNDEFINED = 1;
  const WEIGHT_PRIMITIVE = 2;
  const WEIGHT_OBJECT_FUNCTION = 4;
  if (value == null) {
    return WEIGHT_NULL_UNDEFINED;
  } 
  if (typeof value === 'object' || typeof value === 'function') {
    return WEIGHT_OBJECT_FUNCTION;
  }
  return WEIGHT_PRIMITIVE;
}
function getMapValues(map) {  
  return [...map.values()];
}
function getPlainObjectValues(object) {  
  return Object.keys(object).map(function (key) {
    return object[key];
  });
}
function getCollectionValues(collection) {  
  if (collection instanceof Array) {
    return collection;
  }
  if (collection instanceof Map) {
    return getMapValues(collection);
  }
  return getPlainObjectValues(collection);
}
function reduceWeightSum(sum, item) {  
  return sum + getWeightByType(item);
}
function getCollectionWeight(collection) {  
  return getCollectionValues(collection).reduce(reduceWeightSum, 0);
}
let myArray = [null, { }, 15];  
let myMap = new Map([ ['functionKey', function() {}] ]);  
let myObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)  
getCollectionWeight(myMap);    // => 4  
getCollectionWeight(myObject); // => 2  

Это искусство создания небольших и простых функций!


После всех оптимизаций качества кода появляется горсть недурных преимуществ:


  • Читаемость getCollectionWeight() упростилась благодаря не требующему объяснения коду
  • Размер getCollectionWeight() значительно уменьшился
  • Функция getCollectionWeight() теперь защищена от быстрого разрастания, если вы захотите реализовать работу с другими типами коллекций
  • Извлеченные функции теперь — это разгруппированные и переиспользуемые компоненты. Ваш коллега может попросить вас импортировать эти приятные функции в другой проект, и вы сможете это легко сделать.
  • Если случайно функция сгенерирует ошибку, стек вызовов будет более точным, поскольку содержит имена функций. Почти сразу можно обнаружить функцию, которая создает проблемы.
  • Разделённые функции намного проще тестировать и достигать высокого уровня покрытия кода тестами. Вместо того, чтобы тестировать одну раздутую функцию всеми возможными сценариями, вы можете структурировать тесты и проверять каждую маленькую функцию отдельно.
  • Можно использовать формат модулей CommonJS или ES2015. Создавать отдельные модули из извлеченных функций. Это сделает файлы вашего проекта легкими и структурированными.

Эти преимущества помогут вам выжить в сложной структуре приложений.

Общее правило — функции не должны быть больше 20 строк кода. Чем меньше, тем лучше.


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


2. Функции должны быть простыми


Давайте немного отвлечёмся и подумаем, что такое приложение?


Каждое приложение реализует набор требований. Задача разработчика — разделить эти требования на небольшие исполняемые компоненты (области видимости, классы, функции, блоки кода), которые выполняют чётко определенные операции.


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


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


В чём необходимость? Простые функции — очевидны. Очевидность означает лёгкое чтение и модификацию.


Попробуем последовать примеру. Предположим, вы хотите реализовать функцию, которая сохраняет только простые числа (2, 3, 5, 7, 11, и т.д.) массива и удаляет остальные (1, 4, 6, 8, и т.д.). Функция вызывается так:


getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11] 

Какие шаги предыдущего уровня абстракции нужны для реализации функции getOnlyPrime()? Давайте сформулируем так:


Для реализации getOnlyPrime() отфильтруйте массив чисел с помощью функции IsPrime().

Просто примените функцию-фильтр IsPrime() к массиву.


Есть необходимость на этом уровне реализовать детали IsPrime()? Нет, потому что тогда у функции getOnlyPrime() появятся шаги из другого уровня абстракций. Функция примет на себя слишком много задач.


Не забывая эту простую идею, давайте реализуем тело функции getOnlyPrime():


function getOnlyPrime(numbers) {  
  return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]  

Как видите, getOnlyPrime() — элементарная функция. Она содержит шаги из одного уровня абстракции: метод .filter() массива и IsPrime().


Теперь пришло время перейти на предыдущий уровень абстракции.


Метод массива .filter() входит в JavaScript и используется как есть. Конечно, стандарт описывает именно то, что выполняет метод.


Теперь можно конкретизировать то, как будет реализована IsPrime():


Чтобы реализовать функцию IsPrime(), которая проверяет, является ли число n простым, нужно проверить, делится ли n на любое число от 2 до Math.sqrt(n) без остатка.

Давайте напишем код для функции IsPrime(), пользуясь этим алгоритмом (он еще не эффективный, я использовал его для простоты):


function isPrime(number) {  
  if (number === 3 || number === 2) {
    return true;
  }
  if (number === 1) {
    return false;
  }
  for (let divisor = 2; divisor <= Math.sqrt(number); divisor++) {
    if (number % divisor === 0) {
      return false;
    }
  }
  return true;
}
function getOnlyPrime(numbers) {  
  return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]  

getOnlyPrime() — маленькая и элементарная. В ней только строго необходимые шаги предыдущего уровня абстракции.


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


3. Используйте компактные названия функций


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


Для имен функций используйте формат camel case, который начинается с маленькой буквы: addItem(), saveToStore() или getFirstName().


Поскольку функция — это действие, её имя должно содержать, как минимум, один глагол. Например deletePage(), verifyCredentials(). Чтобы получить или установить свойство, используйте префиксы set и get: getLastName() или setLastName().


В production избегайте запутывающие имена, вроде Foo(), bar(), а(), fun() и подобные. Такие имена не имеют смысла.


Если функции маленькие и простые, а имена компактные: код читается как хорошая книга.


4. Вывод


Конечно, приведенные примеры незамысловаты. Приложения, существующие в реальности, более сложные. Можно жаловаться, что писать простые функции предыдущего уровня абстракции — нудное занятие. Но оно не настолько трудоёмкое если делать это с самого начала проекта.


Если в приложении уже есть слишком раздутые функции, перестроить код, скорее всего, будет сложно. И во многих случаях невозможно в разумных временных промежутках. Начните, хотя бы с малого: извлеките то, что сможете.


Конечно, правильное решение — грамотно реализовать приложение с самого начала. И вложить время не только в реализацию, но и в правильную структуру функций: сделать их маленькими и простыми.


Семь раз отмерь, один раз отрежь.


В ES2015 реализована хорошая модульная система, которая четко показывает, что небольшие функции — это хорошая практика.


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


Нет ничего хуже грязного кода.


Какие методы используете вы, чтобы сделать код организованным?


(Перевод Наталии Басс)

Поделиться с друзьями
-->

Комментарии (49)


  1. napa3um
    21.09.2016 12:30
    +25

    Вот мы и дожили до переоткрытия структурного программирования (нисходящего проектирования). Скоро кто-то догадается, что функции можно компоновать с данными, и даже описывать классы таких компоновок.


  1. potan
    21.09.2016 13:26
    -5

    Язык должен способствовать написанию маленьких функций. Хотя бы что бы на каждую не требовалось писать длинные слова function и по несколько return.


    1. playermet
      21.09.2016 16:32
      +5

      Маленькая функция — это не та, в которой мало буков, а та в которой мало действий.


      1. potan
        21.09.2016 17:21
        +2

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


  1. iCpu
    21.09.2016 13:38
    -9

    Интересно, а как это влияет на скорость работы кода? Даже компилируемые языки с их оптимизациями и встраиванием не могут 100% оптимизировать код, а уж в жабоскрипте…


    1. vlreshet
      21.09.2016 13:51
      +2

      А вы можете обосновать чем компилятору сложнее оптимизировать кучку маленьких функций чем одну здоровую простыню?


      1. dimoclus
        21.09.2016 13:54

        Здоровая простыня находится в одной единице трансляции, а куча маленьких файлов может быть разбросана по различным объектным файлам. С подобным может справиться ICC, но на больших проектах это приведет к взрывному росту времени компиляции.


      1. iCpu
        21.09.2016 16:22
        -1

        Прежде всего, мой вопрос касался интерпретируемых языков, в частности, JavaScript. Не нужно быть гением, чтобы понимать, что чем больше нам нужно сделать переходов по DOM-дереву в поисках функции, тем дольше будет выполняться код. И чем меньше операций выполняет каждая вызываемая единица, тем больше становятся накладные расходы. Я поинтересовался конкретными цифрами, с чего вы сагрились, я не понимаю.

        Теперь про оптимизации. Я не могу показать, что увеличение числа функций усложняет оптимизацию кода. Но достаточно очевидно, что оно их не упрощает. То есть в теории нам нет разницы, сколько вызовов функций есть, максима оптимизации кода всегда одна (или множество равнооптимальных). На практике же мы ограничены очень многими вещами, начиная от объёмов используемой памяти и заканчивая временем компиляции, а так же свойствами среды, такими, как многопоточное исполнение кода.
        То есть, например, в том месте, где программисту очевидно, что эта данная (локальная/глобальная) переменная/поле объекта используется на запись только в одном потоке, компилятор такими знаниями не обладает. В том месте, где программист знает, что между вызовами двух процедур не произойдёт ничего, компилятор этого не знает. В конце концов, в том месте, где программист может в голове раскрутить рекурсивный вызов метода в цикл, компилятору может тупо не хватить глубины дерева, и он оставит не оптимальный вариант кода.
        Я даже не говорю о том, что простое расчленение функции далеко не всегда корректно: очень часто приходится проверять различные предусловия, вроде проверки ссылки на null. Оставлять синтаксические единицы, которые не проверяют предусловия не корректно, а если проверки вставить в каждую функцию, компилятор далеко не всегда сможет понять, что между начальной и конечной точкой объект не сменит своё состояние, и оставит лишние, с точки зрения программиста, проверки.
        Вот вам конкретный пример, хоть и немного отвлечённый от основной темы вашего вопроса: https://habrahabr.ru/post/309796/

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


        1. quasilyte
          21.09.2016 20:41
          -1

          Компилятор либо встроит тело функции, если её тело доступно, либо вставит вызов. Когда он вставит тело функции, то ему нет разницы, была ли это маленькая функция или часть одной большой функции. Специфика встраивания сильно зависит от компилятора и от языка программирования, но в любом случае, у вас есть выбор, например, в релизе компоновать все исходники в 1 единицу трансляции и компилировать как одно целое; либо можно без этого воспользоваться LTO. В общем, я не думаю, что компилятору «сложнее».

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

          А в интерпретируемых языках, где нет нормального JIT, я бы о таких мелочах не сильно заботился, там в одной версии i += 1 может быть медленнее, чем ++i, а потом эта разница может быть нивелирована. Очевидную, вероятно, вещь написал…


          1. iCpu
            21.09.2016 21:09
            +1

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

            И что, разве это имеет отношение к встраиванию? Да нет, не имеет, как в доме искали, так и будут искать. От силы будут оптимизации вроде кеширования адреса.

            Так-то мне всё равно, однако меня нервируют подобные призывы «делайте больше функций» в разделе интерпретируемых языков. У меня и без того хром лагает. При этом, если бы в статье написали про то, что это счастье не бесплатное, то и ладно. Ан нет, не написали.


            1. playermet
              22.09.2016 11:35

              > Так-то мне всё равно, однако меня нервируют подобные призывы «делайте больше функций» в разделе интерпретируемых языков
              Пишу на luajit. Призываю — делайте больше функций. Маленькие функции оптимизируются лучше больших, потому что дают оптимизатору больше информации о структуре кода.

              Для V8 это тоже справедливо.


              1. iCpu
                22.09.2016 11:58

                https://habrahabr.ru/post/310590/#comment_9822368
                Для V8 всё очень спорно. В каком-то месте всё работаает быстрее. В каком-то — медленнее. Стабильного результата тестов добиться, пожалуй, сложнее всего.
                За остальных говорить не буду. Если для языков с сильной оптимизацией подготовка кода к оптимизации больше похоже на массонство, то для динамических языков это скорее техники вуду, в которых все твои действия направлены на ублажения духов. А что они пожелают в этот раз, фруктов или девственницу, и понравятся ли им твои дары — совершенно не известно.


        1. amakhrov
          22.09.2016 07:57
          +1

          мой вопрос касался интерпретируемых языков, в частности, JavaScript.

          JS — очень даже компилируется. V8 компилирует его в нативный код, Rhino в байт-код JVM, etc.


          переходов по DOM-дереву в поисках функции

          DOM не имеет никакого отношения компиляции и выполнению кода


          Теперь про оптимизации.

          Раз был упомянут JS, давайте на примере движка V8. Есть ряд способов помешать V8 оптимизировать функцию (см, допустим, тут — https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments). К примеру, нам нужно использовать try/catch при парсинге json. Если такое происходит в большой функции, то она вся не оптимизируется. А если ее разбить на небольшие функции, то бОльшая часть из них будет оптимизирована. Неоптимизированной останется только та, в которой остался наш try/catch


          1. iCpu
            22.09.2016 08:15
            -1

            >V8 компилирует его в нативный код
            Транслирует. Нет, конечно, сейчас могут набежать всякие умники и кричать «Ты не знаешь, что такое трансляция!», «Да ты не знаешь терминов!», «Кто тебя вообще на хабр пустил?!». А я под их крики и бурления минусов получаю из .class переоптимизированных файлов код, практически эквивалентный начальному. Нет, конечно, ряд оптимизаций выполняется на ходу, и из кешей можно достать весьма оптимальные… Нет, без смеха я не могу подобную хрень писать!

            DOM очень даже имеет отношение, так как .js файлы нужно _парсить_. Быть может, я не до конца в тренде, и в v8 уже всё поменялось…

            Хорошо, покажите мне цифры.


            1. amakhrov
              22.09.2016 08:51
              +1

              Транслирует. Нет, конечно, сейчас могут набежать всякие умники и кричать

              А могут и не кричать, а попросить внятно объяснить разницу между трансляцией и компиляцией, как вы ее видите. Ибо в моем понимании преобразование JS кода в машинный (ассемблер) — это именно что компиляция.


              DOM очень даже имеет отношение, так как .js файлы нужно парсить

              Ок, нужно парсить. А DOM (Document Object Model) при чем? Или речь о чем-то вроде AST (Abstract Syntax Tree)?


              Хорошо, покажите мне цифры.

              https://gist.github.com/amakhrov/e52a9c1430d2103f676c75118aa5eba6
              Для простоты обе функции объявлены в одном файле, но для бОльшей изоляции каждый раз запускаю только одну из них (последние две строчки — раскомментарена только одна)


              -> node -v
              v6.3.1
              
              -> node perf.js
              "mainSeparate()" duration: 9922ms
              
              -> node perf.js
              "mainInline()" duration: 10148ms

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


              1. iCpu
                22.09.2016 10:58

                Действительно, исключения замедляют большие куски, и это не вполне логично, учитывая отсутствие оптимизаций. С другой стороны, мы сравниваем немного не то, что я хочу сравнить. Хотя это тоже интересно и я капельку развил ваш код, но меня интересовали именно налкадные расходы от вызова функций, которые замечательно инлайнятся в нормальных компилируемых языках. (Ведь компиляция — это не просто трансляция в низкоуровневый код (потенциально обратимая), но и применение различных методов оптимизации транслируемого кода) То, что я увидел, наводит на размышления, потому что код действительно оптимизируется, хотя эти оптимизации и не выходят за рамки функции\класса. Это вполне логично для динамического языка, а результат в просадке на 6-8% при 10 вызовах на 10 умножений и одно взятие даты — это хороший результат.
                http://pastebin.com/JrzYQngS

                «mainSeparate()» duration: 1787.7ms
                «mainInline()» duration: 1919.7ms
                «mainSeparateNoExc()» duration: 1878ms
                «mainInlineNoExc()» duration: 2004.2ms
                «Mult1()» duration: 305.8ms
                «Mult2()» duration: 257ms
                «Mult1()» duration: 277.7ms
                «Mult2()» duration: 261.6ms
                «Mult1()» duration: 261.2ms
                «Mult2()» duration: 236.4ms
                «Mult1()» duration: 256.8ms
                «Mult2()» duration: 258.5ms
                «Mult1()» duration: 248.8ms
                «Mult2()» duration: 233.3ms
                «Mult1()» duration: 248.8ms
                «Mult2()» duration: 231.4ms
                «Mult1()» duration: 265.1ms
                «Mult2()» duration: 232.6ms
                «Mult1()» duration: 272.3ms
                «Mult2()» duration: 231.8ms
                «Mult1()» duration: 248.9ms
                «Mult2()» duration: 231.9ms
                «Mult1()» duration: 246.9ms
                «Mult2()» duration: 231.2ms

                >node -v
                v6.6.0

                Upd: Поторопился, простой вызов Date.now() отрабатывает ещё медленнее. К чёрту JS


                1. iCpu
                  22.09.2016 11:18

                  Upd2: Был просто выброс, львиную долю времени выполняется Date.new(), внося недопустимо большие помехи.избавляемся и смотрим на результат.
                  http://pastebin.com/d7dns8yL

                  «Mult1()» duration: 15.8ms
                  «Mult2()» duration: 123ms
                  «Mult3()» duration: 123.1ms
                  «Mult1()» duration: 137.9ms
                  «Mult2()» duration: 123ms
                  «Mult3()» duration: 122.8ms
                  «Mult1()» duration: 137ms
                  «Mult2()» duration: 123.1ms
                  «Mult3()» duration: 122.9ms
                  «Mult1()» duration: 137ms
                  «Mult2()» duration: 122.9ms
                  «Mult3()» duration: 122.9ms
                  «Mult1()» duration: 136.8ms
                  «Mult2()» duration: 123.3ms
                  «Mult3()» duration: 122.9ms
                  «Mult1()» duration: 137ms
                  «Mult2()» duration: 122.9ms
                  «Mult3()» duration: 123.2ms
                  «Mult1()» duration: 137.9ms
                  «Mult2()» duration: 123.2ms
                  «Mult3()» duration: 123.1ms
                  «Mult1()» duration: 137.2ms
                  «Mult2()» duration: 131ms
                  «Mult3()» duration: 136ms
                  «Mult1()» duration: 138.1ms
                  «Mult2()» duration: 122.7ms
                  «Mult3()» duration: 122.6ms
                  «Mult1()» duration: 136.8ms
                  «Mult2()» duration: 122.5ms
                  «Mult3()» duration: 122.6ms

                  Из которого видно, что накладные вызовы 10 вызовов методов составляют примерно 10% про полном отсутствии остального кода. Не то, чтобы это было много.


                1. amakhrov
                  22.09.2016 18:26

                  которые замечательно инлайнятся в нормальных компилируемых языках

                  Какой-нибудь полиморфный вызов метода в джаве — и инлайна уже не будет.


                  С другой стороны, V8 тоже отлично умеет инлайнить. Напр., http://www.mattzeunert.com/2015/08/21/toggling-v8-function-inlining-with-node.html


                  1. iCpu
                    23.09.2016 00:27
                    -1

                    Вы хоть читаете комменты или бомбите наугад? https://habrahabr.ru/post/310590/#comment_9821434

                    Меня жаваскрипт интересует прежде всего как браузерный скриптодвижок. Насколько я могу судить, node.js в плане оптимизаций далеко впереди браузерной версии, как минимум, потому, что у браузера есть всего 3 секунды на разбор полотна скриптов, что не даёт особого простора для оптимизации.


                    1. amakhrov
                      23.09.2016 00:37

                      Вы хоть читаете комменты или бомбите наугад?

                      За всеми ветками комментов разве уследишь? С одной бы разобраться.


                      у браузера есть всего 3 секунды на разбор полотна скриптов

                      Код оптимизируется в ран-тайме, используя ран-тайм статистику (в частности, то, как часто вызывается та или иная функция).


    1. kahi4
      21.09.2016 19:43
      +1

      https://github.com/mrdoob/three.js/pull/8938 Я всего лишь избавился от вызова функции, сделав её, условно говоря, инлайновой, получил от этого более 20% прироста производительности. В действительности, зависит от задач. На скорость выполнения это безусловно влияет и в худшую сторону, иногда даже сильно, покуда вызов функции обходится дорого. С другой стороны, есть обратная сторона медали. v8 не может компилировать функции в нативный код если у них выбрасывается исключение (есть try-catch блок, точнее). Таким образом, если в одной большой функции будет try-catch блок, то не будет оптимизирована вся логика, а если часть этой функции вынести — то можно получить большой прирост. Но это уже совсем ниньзя-техники оптимизации.


      1. iCpu
        21.09.2016 20:12

        Спасибо, мил человек! А то собрались циники «Какие ваши докозательства?»


      1. amakhrov
        22.09.2016 08:11

        Строго говоря, вы не просто заинлайнили функцию. Вы еще и общую логику кода изменили.
        было:


        te[0] = a;
        t[0] = t[0] * detInv; // посредством вызова метода

        стало:


        te[0] = a * detInv;

        На одно чтение/запись элемента массива меньше — это само по себе оптимизация.


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


        1. kahi4
          22.09.2016 18:37

          Безусловно. Однако так или иначе, вызов функции — операция крайне накладная: много операций со стеком, виртуальными таблицами (не всегда, зависит от реализации), промахи по кешу, сброс предсказателя ветвлений — скрытых эффектов вагон и целая тележка, которая еще и слабо контролируется (в случае с js — никак не контролируется)


  1. maxru
    21.09.2016 13:54
    +1

    Однажды мой коллега уволился, потому что пытался справиться с REST API на Ruby, который было трудно поддерживать.

    Слабак :)


    1. il--ya
      21.09.2016 14:02
      +5

      Слабак :)

      Слабак остался бы, и преумножал г-но.


      1. maxru
        21.09.2016 14:08
        -2

        Это другой вариант слабака.

        Если бы было написано, что-то вроде «Однажды мой коллега уволился, потому что пытался справиться с REST API на Ruby, который было трудно поддерживать, но руководство не разрешило выделить время на рефакторинг», то я бы не написал «слабак» :)


        1. il--ya
          21.09.2016 14:18
          +3

          но руководство не разрешило выделить время на рефакторинг

          Там так и написано: «Клиент не хотел перестраивать приложение, делать ему удобную структуру, и разработчик принял правильное решение — уволиться.»


        1. sentyaev
          21.09.2016 16:46
          +1

          руководство не разрешило выделить время на рефакторинг

          Так оно и не должно. Это же детали, хороший код или плохой, это ответственность программиста.
          Я тут согласен с Робертом Мартином, тесты и рефакторинг это не проблема бизнеса, это ответственность программиста, и разрешения на это получать не нужно.
          А если вам бизнес или менеджер говорит — не пиши тесты, не рефактори, т.е. явно запрещает, то да, делать на такой работе нечего.


          1. maxru
            22.09.2016 13:24

            Я имел в виду технического руководителя, конечно же (если он есть).


            1. sentyaev
              22.09.2016 13:35

              Как я считаю, это не ответственность тимлида/техлида. Это ответственность исключительно конкретного программиста.
              Ведь будет странно если вам запретять использовать например наследование или постфиксный оператор.


              1. maxru
                23.09.2016 13:09

                Бизнес слабо интересует рефакторинг, рефакторинг бизнесу нужно «продать», либо выудить время на него ещё каким-либо образом (перезакладываться по фиче-задачам, например).

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


                1. sentyaev
                  23.09.2016 15:20

                  На самом деле рефакторинг, ради рефакторинка я бы и сам не купил)
                  Особенно когда нужен не просто рефакторинг, а еще и ручное тестирование (это если автотестов нет).

                  А вот если тесты у вас есть, то про рефакторинг вы ни у кого и спрашивать не будете, т.к. у вас есть доказательство корректности кода.

                  Если у вас большая легаси система без тестов и с плохим кодом, есть замечательная книга «Working Effectively with Legacy Code».

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


  1. capslocky
    21.09.2016 16:31

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


    1. iCpu
      21.09.2016 16:33
      +2

      ~~Теоретически~~


    1. sentyaev
      21.09.2016 16:54

      Особенно если тесты написаны.


      1. Igelko
        23.09.2016 08:06

        только при условии, что тесты написаны. и они есть на вышележащие слои тоже.


  1. Hint
    21.09.2016 17:52
    +4

    Если говорить о примере из первой главы, то на мой взгляд стало хуже. Была одна довольно маленькая функция с линейным выполнением (посмотрел на неё и сразу всё понял), а стало много мелких связанных друг с другом функций (бегаешь глазами от одной к другой). Если функция часто используется, а не один вызов на запрос, то её быстродействие существенно просядет за счет множества внутренних вызовов.

    Очевидно, что нужно разбивать сложные функции на части. Но у меня часто возникает проблема из-за того, что внутренние функции очевидно не будут где-либо повторно использованы. Довольно часто это специфическая функция со специфическим набором параметров и специфическим результатом. И как её тогда называть? Использовать имя родительской функции в качестве префикса? Более того, если мы выносим код в отдельную функцию, то появляется вопрос проверки входящих параметров, который наверняка выполнялся в родительской функции. В родительской функции мы точно знали, что параметры верные, а тут получается мы или ничего не проверяем, что плохо для самостоятельности новоиспеченной функции, или в очередной раз проверяем то, что уже проверяли раньше, что негативно сказывается на быстродействии. В основном я программирую на php и там у меня с этим совсем беда, так как в языке нет локальных функций. Их наличие могло исправить ситуацию за счет того, что внутренние функции имеют локальную область видимости, а значит не должны иметь уникальные понятные имена, плюс не могут быть вызваны из вне, а значит нет смысла по несколько раз проверять параметры.


    1. CodeRush
      21.09.2016 20:28

      Более того, там при рефакторинге закралась копипаста вот здесь, и код переписаный просто не работает:

      let myArray = [null, { }, 15];  
      let myMap = new Map([ ['functionKey', function() {}] ]);  
      let myObject = { 'stringKey': 'Hello world' };  
      getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)  
      getCollectionWeight(myMap);    // => 4  
      getCollectionWeight(myObject); // => 2  
      


    1. sumanai
      21.09.2016 20:45

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

      Замыкания?


  1. i86com
    21.09.2016 20:41
    +7

    Мне уже страшно от того, что через пару лет придётся разгребать код людей, вдохновившихся этой статьёй. Особенно при доведении этих принципов до абсолюта.

    function getMapValues(map) {  
      return [...map.values()];
    }
    


    Вот это вообще гениально. На ровном месте создаётся обёртка для синтаксиса языка, которая не несёт вообще никакой пользы.

    Просто представьте, что через пару лет после написания этого кода у вас возникла ошибка — вы захотели вывести несколько коллекций в порядке возрастания суммы, но результат вас удивил — порядок явно не тот. В чём проблема — неправильные исходные данные ('null' вместо null), ошибка в сортировке или в функции определения веса?
    Вы начинаете копать функцию определения веса и видите этот спагетти-код из микро-функций, которые разбросаны по разным местам разных файлов вашего проекта (код-то «переиспользуемый»). И каждый раз, встретив функцию, вам нужно её индивидуально искать, разбираться, что она делает, и потом возвращаться к тому месту, где остановились.

    Не забывайте, мы лишь рассматривали простую функцию подсчёта веса коллекций. Примитив. А теперь представьте, что у нас что-то сложнее и ближе к реальному миру — например, функция, которая определяет, сколько нужно коробок, чтобы переслать N предметов разных габаритов (с учётом всех нюансов — габаритов, ограничений по весу, требований не класть в одну коробку тяжёлое с хрупким, таможенных особенностей). Сколько там можно написать микро-функций? Сколько времени у вас займёт найти среди них ошибку округления?


  1. DmitryMironov
    21.09.2016 20:41

    делать ему удобную структуру

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


  1. spinmozg
    21.09.2016 20:41
    +1

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

    Ситуация напоминает ситуацию с нормализации реляционных баз данных: вроде теоретически для каждого факта должна быть своя таблица с внешними ключами, но на практике это ужасно неудобно. Думаю, во всем должно быть чувство меры…


  1. KennyGin
    21.09.2016 20:46
    +1

    А с каких пор JS стал дефолт-языком? Хаба соотвествущего у статьи нет, даже тегов. Однако специфичная терминология присутствует:

    К счастью, ES2015 позволяет объявить const как read-only,
    Можно использовать формат модулей CommonJS


  1. Magnetic_Air
    22.09.2016 09:20
    +1

    Правда как всегда находится где-то посередине. Не нужно кидаться в крайности, нужно просто находить компромисс между производительностью и «удобством/красотой» кода. Но понимание этого приходит только после того как «пощупаешь» и то и другое…


  1. mmMike
    22.09.2016 12:30

    Проблема хорошо видна. Функция getCollectionWeight() слишком раздутая и выглядит как черный ящик, полный сюрпризов.

    Проблема отчетливо видна в другом. Ни одного комментария в коде по поводу что эта функция делает, stateless она или нет и прочие детали.


    Лично мне плевать как функция написана внутри, если я знаю, что она протестирована и работает так как описана.


    Если сильно приспичит — разберусь и с разбитой на подфункции и с непрерывным кодом одним блоком.
    Особенно если код разбит осмысленно (здравый смысл) на блоки и прокомментирован.


  1. stranger777
    22.09.2016 13:30

    В статье есть указание на get/set с ясностью и тут же let myMap, let my…
    А мой — это какой и даже точнее — чей? К такой переменной, которая моя, хочется обращаться отовсюду: моя же, что хочу, то и делаю; а она хрясь и локальная. Логичнее префикс loc[al].
    Насчёт двадцати строк тоже сомнительно: МакКонелл писал о семи, ссылаясь на свойство внимания и памяти о семи объектах.
    Ну и, наконец, видно и написано, что статья — концентрат содержания других источников. Хорошо бы ссылки для полноты.


  1. playermet
    22.09.2016 20:29

    > МакКонелл писал о семи, ссылаясь на свойство внимания и памяти о семи объектах.
    Я думаю отступы и скобки блоков в их число не входили, а с ними это как раз и будет около 20 реальных строк.


  1. Igelko
    22.09.2016 22:00

    Аргументы за простые и маленькие функции:
    1) Каждую в отдельности легко прочитать. Меньше сущностей нужно уложить в голову, когда воссоздаётся модель поведения.
    2) Легче покрывать код тестами — относительно мало инвариантов, которые нужно проверять.


    Против:
    1) Увеличение связности кода. Т.е. меняя функцию в глубине, можно сломать вышележащие. На самом деле сложность из самой функции перемещается в связи между ними. У каждого кусочка кода растёт количество способов его использования. Может возникнуть ситуация, когда приходится какой-то кусочек обобщать настолько, что на самом деле было бы проще раскопипастить частные случаи выше по стеку. (Нарушили случайно single responsibility и ой).


    2) Большое количество переключений контекста при написании кода (не очень страшно) и багфиксинге, который будут делать скорее всего другие люди и через полгода-год (а вот это беда). Т.е. чтобы понять почему вылетело то или иное исключение нужно пройтись сверху донизу (или снизу доверху), собрав в голове историю создания контекста в месте падения. IDE и дебаггеры немного купируют эту проблему, но не до конца.