[Перевод] Чистый javascript.Функции

Перевод книги Райана Макдермота clean-code-javascript.

Оглавление:


  • Введение
  • Переменные
  • Функции

0f0b5f8f6a0ee319885ca448d6582706.png

Аргументы функции (идеально 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}`);
}

Избегайте побочных эффектов


Функция производит побочный эффект, если она совершает какое-либо действие помимо получения значения и возврата другого значения или значений. Побочный эффект может быть записью в файл, изменением каких-то глобальных переменных или случайным переводом всех ваших денег неизвестным лицам.
Впрочем, побочные эффекты в программе необходимы. Пусть, как и в предыдущем примере, вам требуется запись в файл. Опишите то, что вы хотите сделать, строго в одном месте.
Не создавайте несколько функций и классов, которые пишут что-то в конкретный файл. Создайте один сервис, который всем этим занимается. Один и только один.
Суть в том, чтобы избегать распространенных ошибок, таких как, например, передача состояния между объектами без какой-либо структуры, с помощью изменяемых данных, которые может перезаписывать кто угодно, в обход централизованного места применения побочных эффектов.
Если научитесь так делать, вы станете счастливее, чем подавляющее большинство других программистов.
Плохо:
// Глобальная переменная, на которую ссылается последующая функция.
// Если бы у нас была еще одна функция, которая бы работала с именем 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'];

Не переопределяйте глобальные функции


Загрязнение глобальных переменных — плохая практика в 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');

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

  • 15 января 2017 в 19:46

    +3

    >Идеальная ситуация — отсутствие аргументов.

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

  • 15 января 2017 в 21:25

    0

    Избегайте необдуманного следования правилам моды. Сегодня они помогут, а завтра подведут.

  • 15 января 2017 в 21:27

    0

    Вот бы ещё сглаживание тексту на иллюстрации к статье…
    • 15 января 2017 в 21:29

      0

      Вот ещё бы без ненужных иллюстраций к статье.
    • 15 января 2017 в 21:47

      0

      Заменил

© Habrahabr.ru