Оглавление:
- Введение
- Переменные
- Функции
- Классы
- Объекты и структуры данных. Асинхронность. Обработка ошибок.
- Тестирование. Форматирование. Комментарии.
Аргументы функции (идеально 2 или менее)
Ограничение количества параметров функции невероятно важно, поскольку оно упрощает тестирование функции. Наличие более чем трёх аргументов приводит к комбинаторному взрыву, когда вам приходится перебирать массу различных случаев с каждым отдельным аргументом.
Идеальная ситуация — отсутствие аргументов. Один или два аргумента — хорошо, а трех уже следует избегать.
Большее количество аргументов необходимо консолидировать. Как правило, если передается более двух аргументов, ваша функция пытается сделать слишком многое. В тех случаях, когда это все же не так, лучше использовать объект в качестве аргумента. Поскольку JavaScript позволяет создавать объекты на лету, без специального описания классов, их вполне можно применять, когда требуется передать множество аргументов.
Плохо:
function createMenu(title, body, buttonText, cancellable) {
// ...
}
Хорошо:
const menuConfig = {
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
};
function createMenu(config) {
// ...
}
createMenu(menuConfig);
Функция должна решать одну задачу
Это, безусловно, самое важное правило в разработке программного обеспечения. Когда функции решают более одной задачи, их труднее сочетать, тестировать и понимать. Как только вы сможете свести каждую функцию к выполнению только одного действия, их станет значительно проще рефакторить, а ваш код станет гораздо более читаемым. Даже если приведенное правило будет единственным вынесенным вами из этого руководства, вы все равно будете круче многих разработчиков.
Плохо:
function emailClients(clients) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
Хорошо:
function emailClients(clients) {
clients
.filter(isClientActive)
.forEach(email);
}
function isClientActive(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
Названия функций должны описывать их назначение
Плохо:
function addToDate(date, month) {
// ...
}
const date = new Date();
// По имени функции трудно сказать, что именно добавляется
addToDate(date, 1);
Хорошо:
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);
Функции должны представлять только один уровень абстракции
Если в функции представлено более одного уровня абстракции, то она, как правило, делает слишком многое. Разделение таких функций приведет к возможности повторного использования и облегчению тестирования.
Плохо:
function parseBetterJSAlternative(code) {
const REGEXES = [
// ...
];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
// ...
});
});
const ast = [];
tokens.forEach((token) => {
// правило...
});
ast.forEach((node) => {
// парсинг...
});
}
Хорошо:
function tokenize(code) {
const REGEXES = [
// ...
];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
tokens.push( /* ... */ );
});
});
return tokens;
}
function lexer(tokens) {
const ast = [];
tokens.forEach((token) => {
ast.push( /* ... */ );
});
return ast;
}
function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const ast = lexer(tokens);
ast.forEach((node) => {
// парсинг...
});
}
Избавляйтесь от дублированного кода
Изо всех сил старайтесь избегать дублированного кода. Дублированный код вреден тем, что подразумевает наличие более чем одного места, в которое придется вносить правки, если логика действий изменится.
Представьте, что управляете рестораном и ведете учет всех продуктов — помидоров, лука, чеснока, специй и т.д. Если их учет ведется в разных списках, то подача любого блюда с помидорами потребует внесения изменений в каждый список. Если же список только один, то и правка будет всего одна!
Зачастую дублированный код возникает в тех случаях, когда требуется реализовать два или более незначительно различающихся действия, которые в целом очень схожи, но их различия вынуждают вас завести две или более функции, делающих практически одно и то же. В этом случае избавление от дублированного кода будет означать создание абстракции, которая сможет представить все различия в виде одной функции, класса или модуля.
Создание правильной абстракции — вопрос невероятной важности, и именно поэтому вы должны следовать принципам SOLID. Плохие абстракции могут оказаться хуже дублированного кода, так что будьте осторожны!
Подводя итог: если можете обернуть код хорошей абстракцией — так и сделайте! Не дублируйте код, иначе вам придется вносить множество правок на каждое небольшое изменение.
Плохо:
function showDeveloperList(developers) {
developers.forEach((developer) => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach((manager) => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
Хорошо:
function showList(employees) {
employees.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
let portfolio = employee.getGithubLink();
if (employee.type === 'manager') {
portfolio = employee.getMBAProjects();
}
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
Устанавливайте объекты по умолчанию с помощию Object.assign
Плохо:
const menuConfig = {
title: null,
body: 'Bar',
buttonText: null,
cancellable: true
};
function createMenu(config) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz';
config.cancellable = config.cancellable === undefined ? config.cancellable : true;
}
createMenu(menuConfig);
Хорошо:
const menuConfig = {
title: 'Order',
// Пользователь не имеет свойства 'body'
buttonText: 'Send',
cancellable: true
};
function createMenu(config) {
config = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
// теперь config = {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}
createMenu(menuConfig);
Не используйте флаги в качестве параметров функции
Флаги говорят пользователю, что функция совершает более одного действия. Функция должна решать одну задачу. Разделяйте функции, если они исполняют различные варианты кода на основе логического значения.
Плохо:
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
Хорошо:
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
Избегайте побочных эффектов (Часть 1)
Функция производит побочный эффект, если она совершает какое-либо действие помимо получения значения и возврата другого значения или значений. Побочный эффект может быть записью в файл, изменением каких-то глобальных переменных или случайным переводом всех ваших денег неизвестным лицам.
Впрочем, побочные эффекты в программе необходимы. Пусть, как и в предыдущем примере, вам требуется запись в файл. Опишите то, что вы хотите сделать, строго в одном месте.
Не создавайте несколько функций и классов, которые пишут что-то в конкретный файл. Создайте один сервис, который всем этим занимается. Один и только один.
Суть в том, чтобы избегать распространенных ошибок, таких как, например, передача состояния между объектами без какой-либо структуры, с помощью изменяемых данных, которые может перезаписывать кто угодно, в обход централизованного места применения побочных эффектов.
Если научитесь так делать, вы станете счастливее, чем подавляющее большинство других программистов.
Плохо:
// Глобальная переменная, на которую ссылается последующая функция.
// Если бы у нас была еще одна функция, которая бы работала с именем name как со строкой,
// то обнаружив массив, он непременно бы поломалась
let name = 'Ryan McDermott';
function splitIntoFirstAndLastName() {
name = name.split(' ');
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
Хорошо:
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}
const name = 'Ryan McDermott';
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];
Избегайте побочных эффектов (Часть 2)
В JavaScript примитивы передаются по значению, а объекты и массивы передаются по
ссылке. В случае объектов и массивов, если наша функция вносит изменения
в корзину (массив), например, путем добавления элемента в массив,
то любая другая функция, которая использует эту корзину (массив) будет зависеть от этого
добавления. Это может быть и хорошо и плохо в разных случаях. Давайте представим себе плохую ситуация:
Пользователь нажимает на кнопку «Покупка», которая вызывает функцию `purchase`, что отправляет данные из корзины (массив) на сервер. В случае плохого подключения к сети `функция purchase` должена отправить повторный запрос. Теперь, что, если в то же время пользователь случайно нажимает кнопку «Добавить в корзину», но пока не хочет покупать товар?
Если это произойдет, и начинается запрос сети, то функция `purchase`
пошлет случайно добавленный элемент, поскольку он имеет ссылку на предыдущую корзину (массив), модифицированую функцией `addItemToCart`. Отличное решение было бы для `addItemToCart` всегда клонировать корзину, отредактировать и вернуть клон. Это гарантирует, что никакие другие функции, которые зависят от корзины не будут зависеть от каких-либо изменений.
Два предостережения по-поводу такого подхода:
- Возможны случаи, когда вы на самом деле хотите изменить объект по ссылке, но такие случаи крайне редки. Большинство функций могут быть объявлены без сайд эффектов!
- Клонирование больших объектов может быть очень нагрузочным и влиять на производительность. К счастью, это не является большой проблемой на практике, потому что есть отличные библиотеки, которые позволяют клонировать объекты с меньшей нагрузкой на память в отличии от клонирования вручную.
Плохо:
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
Хорошо:
const addItemToCart = (cart, item) => {
return [...cart, { item, date : Date.now() }];
};
Не переопределяйте глобальные функции
Загрязнение глобальных переменных — плохая практика в JavaScript, так как может породить конфликты с другой библиотекой, и пользователь вашего API не увидит ошибок, пока не получит исключение в продакшене. Давайте рассмотрим пример: что делать, если вы хотите расширить стандартный функционал Array из JavaScript, добавив метод diff, который бы вычислял различие между двумя массивами? Вы должны были бы записать новую функцию в Array.prototype, но тогда она может войти в конфликт с другой библиотекой, которая пыталась сделать то же самое. А если другая библиотека использовала метод diff, чтобы найти разницу между первым и последним элементами массива? Именно поэтому гораздо лучше использовать классы ES2015/ES6 и просто унаследовать нашу реализацию от класса Array.
Плохо:
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};
Хорошо:
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}
Отдавайте предпочтение фунциональному программированию над императивным
JavaScript не настолько функциональный язык, как Haskell, но определенной доли функциональности он не лишен. Функциональные языки чище и их проще тестировать. Применяйте функциональный стиль программирования при возможности.
Плохо:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
let totalOutput = 0;
for (let i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}
Хорошо:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
const totalOutput = programmerOutput
.map((programmer) => programmer.linesOfCode)
.reduce((acc, linesOfCode) => acc + linesOfCode, 0);
Инкапсулируйте условия
Плохо:
if (fsm.state === 'fetching' && isEmpty(listNode)) {
// ...
}
Хорошо:
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
Избегайте негативных условий
Плохо:
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
Хорошо:
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
Избегайте условных конструкций
Такая задача кажется невозможной. Услышав подобное, большинство людей говорят: «Как я должен делать что-либо без выражения if?». Ответ заключается в том, что во многих случаях для достижения тех же целей можно использовать полиморфизм. Второй вопрос, как правило, звучит так: «Хорошо, замечательно, но почему я должен их избегать?». Ответ — предыдущая концепция чистого кода, которую мы узнали: функция должна выполнять только одну задачу. Если у вас есть классы и функции, содержащие конструкцию 'if', вы словно говорите своему пользователю, что ваша функция выполняет больше одной задачи. Помните: одна функция — одна задача.
Плохо:
class Airplane {
// ...
getCruisingAltitude() {
switch (this.type) {
case '777':
return this.getMaxAltitude() - this.getPassengerCount();
case 'Air Force One':
return this.getMaxAltitude();
case 'Cessna':
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
}
Хорошо:
class Airplane {
// ...
}
class Boeing777 extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount();
}
}
class AirForceOne extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude();
}
}
class Cessna extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
Избегайте проверки типов (часть 1)
JavaScript является нетипизированным языком, а это значит, что ваши функции могут принимать аргументы любого типа. Порой вы обжигались этой свободой, что побуждало вас производить проверку типов в ваших функциях. Есть множество способов ее избежать. В первую очередь стоит подумать над согласованным API.
Плохо:
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.peddle(this.currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location('texas'));
}
}
Хорошо:
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}
Избегайте проверки типов (часть 2)
Если вы работаете с базовыми примитивами, такими как строки, целые числа и массивы, и не можете использовать полиморфизм, хотя все еще чувствуете необходимость в проверках типа, вам стоит рассмотреть возможность применения TypeScript. Это отличная альтернатива обычному JavaScript, предоставляющая возможность статической типизации поверх стандартного синтаксиса JavaScript. Проблема с ручной проверкой типов в обычном JavaScript в том, что иллюзия безопасности, которую она создает, никак не компенсируется потерей читабельности из-за многословности кода. Держите ваш код в чистоте, пишите хорошие тесты и делайте эффективные ревизии кода. Или делайте все то же самое, но с помощью TypeScript (который, как я уже сказал, является прекрасной альтернативой!).
Плохо:
function combine(val1, val2) {
if (typeof val1 === 'number' && typeof val2 === 'number' ||
typeof val1 === 'string' && typeof val2 === 'string') {
return val1 + val2;
}
throw new Error('Must be of type String or Number');
}
Хорошо:
function combine(val1, val2) {
return val1 + val2;
}
Не оптимизируйте сверх меры
Современные браузеры производят множество оптимизаций под капотом во время исполнения кода. Оптимизируя код вручную, вы, зачастую, просто тратите свое время. Есть прекрасные ресурсы с описанием ситуаций, когда оптимизация действительно хромает. Поглядывайте на них в свободное время, пока эти проблемы не будут исправлены, если вообще будут, конечно.
Плохо:
// В старых браузерах каждая итерация с некэшированной `list.length` будет дорогостоящей
// из-за перерасчета `list.length`. В современных браузерах это оптимизировано.
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
Хорошо:
for (let i = 0; i < list.length; i++) {
// ...
}
Удаляйте мертвый код
Мертвый код так же плох, как повторяющийся код. Нет никаких причин, чтобы держать его в репозитории. Если код не вызывается, избавьтесь от него!
Он по-прежнему будет в системе контроля версий, если когда-нибудь он все-таки вам понадобится.
Плохо:
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Хорошо:
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Комментарии (43)
Holix
15.01.2017 21:25+3Избегайте необдуманного следования правилам моды. Сегодня они помогут, а завтра подведут.
DistortNeo
15.01.2017 23:11+1Поругался в предыдущей теме, поругаюсь и здесь: чем простой цикл плох? Зачем вместо него городить неочевидную конструкцию map-reduce? И почему именно крокодил в виде
.reduce((acc, linesOfCode) => acc + linesOfCode, 0);
когда достаточно было бы написать
.sum();
ReklatsMasters
15.01.2017 23:24-3Эта конструкция помогает избежать засорения скопа ненужными переменными + занимает меньше места. Хотя и проигрывает в скорости циклу. Конструкция очевидна, если вы знаете es5. А на дворе es6 давно.
kahi4
16.01.2017 01:14+2Let и const (const в конструкции for of) тоже не засоряют скоуп. Меньше места занимает только в коде, в памяти заметно больше, да и то есть ситуации, когда через for код получается компактнее. Это я уже молчу про то, что конструкция reduce в примерах посложнее суммы обычно настолько нечитаемая, что даже вы не то, что через пол года, через пол часа уже не поймете что он делает. Особенно если захотелось написать лаконично.
И в скорости проигрывает не всегда, не настолько интерпретатор js тупой (впрочем, зачастую, это гадание на кофейной гуще, сможет ли он оптимизировать или нет).
Единственное преимущество — все таки в большинство случаев использования map код получается чище и лаконичнее, сконцентрировавшись именно на обрабатываемой переменной, особенно в случае последовательной обработки данных, например — сначала фильтруем, потом сортируем, потом переводим в другой формат и обрабатываем. Лаконичности добавляет анонимная функция, которой можно дать название и вынести в нужный модуль.
faiwer
16.01.2017 08:09+1const в конструкции for of
К сожалению, Firefox упорно падал на любой попытке затолкать const в for. А let переменные там же, не обновлялись при каждой итерации. Это доставило мне много "славных" моментов debug-а, когда я использовал одну из таких переменных внутри новых Promise-ов. Бррр. Возможно такое поведение уже исправили.
for(const a of [1, 2, 3]){ }
// SyntaxError: missing = in const declaration
for(let a of [1, 2, 3]){ setTimeout(() => alert(a)) }
// 3 3 3Хм. Нет. 50.1.0
Shannon
16.01.2017 12:41+1let и const в 51 всё таки починили
for(const i of [1, 2, 3]) console.log(i) 1 2 3
for(let a of [1, 2, 3]) { setTimeout(() => console.log(a)) } 1 2 3
Подстава конечно с let от лисы, хоть об этом и говорится в https://kangax.github.io/compat-table/es6/, но это только если раскрыть ветвь
let/const for/for-in loop iteration scope no
ReklatsMasters
16.01.2017 10:52+3Абсолютно согласен почти со всем. У любой конструкции есть свои преимущества и недостатки. Очевидно, что писать нужно так, чтоб читаемость не страдала.
скорости проигрывает не всегда
Старый добрый
for-i
всё ещё уделывает всех. Если у вас другие данные, приведите тесты. А пока вот мой https://repl.it/FKLZ:
node 6.9.1 (V8 5.1):
for-of: 639.740ms for-i: 439.640ms reduce: 921.640ms
node 7.4.0 (V8 5.4)
for-of: 410.190ms for-i: 66.675ms reduce: 316.617ms
DistortNeo
16.01.2017 01:49+2Так объявите функцию
sum
— сэкономится куча места и существенно вырастет читаемость.
Вообще, обратил внимание, что в обычных языках я никогда не использовал reduce в общем виде (он же Aggregate в C#, например), всегда хватало методов типа Sum, Average, Count и т.д.
lumini
16.01.2017 06:10+1Часто видел Aggregate в чужом коде. Но неизменно оказывалось, что это результат автоматической замены ReSharper'ом цикла foreach.
TheShock
16.01.2017 04:06+6Зачем вместо него городить неочевидную конструкцию map-reduce? И почему именно крокодил в виде
Для того, чтобы восхитить других хипстеров столь модным кодом. Других причин нету.faiwer
16.01.2017 09:32+5Сегодня вот наткнулся на Rethinking JavaScript: Death of the For Loop. Смерть циклам. Заголовки js-статей всё пафоснее и пафоснее. Вскоре появятся слова из фентези, видать. А потом и до таких слов как "хтонический ужас" доберутся.
В статье стандартный набор штампов про, о боже, циклы мутируют переменные, о боже, в циклах сайд эффекты. О боже, такой код жутко не предсказуем, там же сплошные баги. Ааа, мы все умрём.
Shannon
16.01.2017 13:10+2Статье не хватает следующей части под названием «1000 и 1 способ придумать название для расплодившихся на ровном месте функций-однострочников»
Даже cat.name не избежал участи превратиться в
const getName = cat => cat.name
sshikov
16.01.2017 09:13Вообще, простой цикл хуже компонуется. И для него не очевидно его назначение — в виде похожих циклов могут быть записаны очень разные вещи (а зачастую — и много разных вещей в одном цикле).
В случае записи в виде функций это получается более ясно (хотя непривычному глазу — возможно и не сразу).
MrGobus
16.01.2017 10:59+2Простой цикл не просто неплох, он хорош.
Первое это перфоманс, у меня на последнем хроме он опережает foreach в разы.
Замерить можно тут: for vs forEach
Второе, это память, каждая такая супер функция создает новый массив выборки.
Ну и наконец читабельность. С моей точки зрения, код теряет наглядность, особенно если функции проверки вынести далеко от кода вызова или в другой файл.faiwer
16.01.2017 11:04+1Небольшая придирка. Если мне не изменяет память, то циклы в jsperf лучше не тестировать вовсе. Он там, на самом деле, корёжит ваш код в целях каких-то своих измерений и в итоге замеряет чёрт знает что.
faiwer
16.01.2017 08:01+3Некоторые советы несколько спорны. Не в том плане, что они не верны, но уже хотя бы в том, что они не так просты как расписаны в статье. Для примера… Статья пестрит различными доводами в пользу выноса чего бы то ни было в отдельные методы. В частности условий из
if
. В ряде случаев, в особенности где не тривиальная логика, это действительно бывает правильным и удобным. Но если вы внезапно начнёте дотошно применять все эти рекомендации одновременно в вашей кодовой базе, то у вас будут чуть ли не сотни методов, с длинными названиями, с реализацией вырванной из места своего логического контекста. И поди потом разбери этот клубок сложного кода. И внеси потом все необходимые правки. Обычно в кодовой базе по ходу дела приходится переключаться между режимами: "всё стало сложно, разносим по частям, делаем рефакторинг" и "всё стало просто, упрощаем кодовую базу". Нет какого однозначного верного подхода.
Или к примеру довод не писать
switch
-и в угоду полиморфизму. Оно то конечно бывает удобно. Но автор не уточнил, что так стоит делать только и только тогда, когда оно того стоит. Т.е. когда подобных мест у вас несколько и вынос всех этих сущностей в отдельную иерархию с наследованием окупает затраты и делает код понятнее. А вовсе не переусложняет простую кодовую базу. Т.е. далеко не каждый switch нужно вот так вот превращать в груды классов.
А ещё, конечно забавно, как одни советы из одних статьей коррелируют с другими советами из других статей. Скажем статьи про "быстрый" код в JS, часто противоречат статьям про "правильный чистый код". Советы из статей с императивным подходом плохо сочетаются с функциональным.
И ещё. По поводу совета предпочитать функциональный подход. Тут одна беда пришла незаметно. У нас появились долгожданные генераторы и async-и. И вот ну очень уж плохо они дружат с функциональным подходом. Начинается монстро-код с Promise.all и прочими шаманствами.
mayorovp
16.01.2017 09:02Небольшая поправка. Использование Promise.all — это самостоятельный трюк, который не зависит от избранного подхода.
Скорее надо вспомнить про шаманство с reduce, в которые превращается простой императивный цикл. И при для нахождения суммы еще понятно что reduce делает — то при использовании then уже мало кто способен сходу понять что вообще в коде происходит.
sshikov
16.01.2017 09:15+1+1 по поводу switch и полиморфизма. Кроме того, наличие if и вообще ветвлений в коде никаким образом не показывает, что у этого кода два и более назначений. Вовсе нет.
faiwer
16.01.2017 09:26+1Ещё вспомнилось про параметры и объект-параметр. Тут палка о двух концах. Если параметры передаются явно и отдельно, то мы получаем более предсказуемый и однозначный код, нежели когда передаётся объект-параметр. Однако когда число параметров вырастает, скажем, до 5-7, это всё выливается в такой геморрой (особенно если код очень полиморфичен)… Брр… Приходится балансировать, глядя на реальную задачу.
Функции с большим количеством параметров вызывают в моей голове воспоминания об Windows API. Такое ощущение, что тамошнее API писали большие фанаты аргументов.
Ещё касательно совета не использовать флаги. Ну с таким то примером и правда не сложно вынести обёртку в отдельный метод. А что если флаг не заменяется простой заменой аргументов функции?
raveclassic
16.01.2017 11:47Функции с большим количеством параметров вызывают в моей голове воспоминания об Windows API. Такое ощущение, что тамошнее API писали большие фанаты аргументов.
Аж до слез.
mayorovp
16.01.2017 12:48+2Можно же использовать объект-параметр вместе с деструктуризацией. Код будет столь же предсказуемым и однозначным.
faiwer
16.01.2017 13:44Нууу, не совсем. Для этого придётся открыть метод и пробежаться по нему глазами. Если деструктуризация в сигнатуре — то пробежать глазами по ней. А в случае аргументов хватит и взгляда на строчку с вызовом метода. Правда от появления доп. аргументов вы, в любом случае, не застрахованы.
К тому же, ИМХО, деструктуризация в сигнатуре выглядит… ужасно. Честно говоря я не сторонник того, чтобы сигнатура метода содержала в себе столько логики, сколько её сейчас туда пихают. Ещё и дефолтные значения. А учитывая, что деструктуризация бывает многоуровневой...
Для себя лично я решил избегать оной в сигнатурах за исключением простых случаев. Ну и лямбд в цепочках трансформаций.
raveclassic
16.01.2017 14:01предсказуемый и однозначный код
Ну кстати, предсказуемость и однозначность можно получить введением типизации для этих объектов-параметров через flow/ts. Тогда можно положиться на IDE и не держать постоянно в голове всю структуру.
type TFooConfig = { foo: string, bar?: number } export const foo = (config: TFooConfig) => { }
i360u
16.01.2017 14:14Проще объявить конструктор для конфига. Типизация, да еще и с внешними зависимостями, тут может быть не нужна.
raveclassic
16.01.2017 14:18+1А в конструктор этот как значения передавать? Снова через аргументы или через еще один объект?
i360u
16.01.2017 14:35-1А в чем проблема? Берете и передаете так-же, как передавали бы в функцию. Конструктор для сериализации/формализации + подсветка в IDE. Если у вас объект с параметрами формируется где-то рядом в функции, где происходит и вызов метода с этими параметрами, то это все вообще делать не нужно, тем более тащить сюда flow.
mayorovp
16.01.2017 14:11Да ну? Вот есть строка с вызовом метода —
foo(5, 7, 'bar')
. И что можно сказать на нее глядя?faiwer
16.01.2017 14:17Какие-то магические константы? 5, 7, 'bar'? :) Вместо них, в реальном коде, будет что-нибудь типа widget.id, position, mode. И сказать можно будет гораздо большее.
mayorovp
16.01.2017 14:22Вот как раз в тех функциях, которые принимают много параметров, обязательно встретится два-три "магических".
i360u
16.01.2017 12:37+1Устанавливайте объекты по умолчанию с помощию Object.assign
function createMenu(config) { config.title = config.title || 'Foo'; config.body = config.body || 'Bar'; config.buttonText = config.buttonText || 'Baz'; config.cancellable = config.cancellable === undefined ? config.cancellable : true; }
Это не "плохо", это вообще неправильно. Пустая строка может являться корректным значением, но в приведенном коде она заменяется на дефолтное.
Использовать Object.assign тоже не очень правильно, есть риск унаследовать кучу мусора прилетевшую в config например из DOM. Нужно как-то так:
const menuConfig = { title: null, body: 'Bar', buttonText: null, cancellable: true }; Object.keys(menuConfig).forEach((prop) => { if (config.hasOwnProperty(prop)) { menuConfig[prop] = config[prop]; } });
Избегайте негативных условий
Это еще почему?
Избегайте условных конструкций
Если избегать, то уж лучше как-то так:
let doSomethingIf = { value1: () => {doSomething(1);}, value2: () => {doSomething(2);}, value3: () => {doSomething(3);}, }; doSomethingIf[value || 'value1' ]();
raveclassic
16.01.2017 12:45Если избегать, то уж лучше как-то так:
Я надеюсь, вы несерьезно.i360u
16.01.2017 13:03Да, сорри, упустил контекст совета в статье. Нужно было подзаголовок иначе сформулировать… Хотя плодить классы на каждый вариант свойств данных тоже совсем "не айс", ИМХО классы должны оставаться на уровне абстракций.
Apathetic
16.01.2017 22:25+1Поясните, если не затруднит.
mayorovp
16.01.2017 22:29Если уж весь код все равно написан по месту, а не вынесен в отдельные классы — в чем вообще смысл заменять нормальный оператор выбора на подобное выражение? Это ж не дает ни-че-го.
raveclassic
16.01.2017 23:44Да и вообще, если использовать tagged unions и тип never с парой флагов из последнего TS, то это один в один ADT и pattern matching с полноценными exhaustive свитчами, так что все эти нагромождения абстракций ради нагромождения абстракций становятся ни к чему.
sshikov
>Идеальная ситуация — отсутствие аргументов.
Хм. Вообще идеальная функция — это чистая функция. А чистая функция без аргументов — это константа, и смысла в ней не слишком много. Исключения конечно бывают вполне полезными (текущее время, или генератор случайных чисел), но считать что это идеально всегда, было бы перебором.
mayorovp
Текущее время или ГСЧ — это не чистые функции.
sshikov
Я не говорил, что она чистая. Я имел в виду, что она — достаточно редкий пример практически полезной функции без параметров.