Чистые код в функциях в JavaScript

7

Чистые функции

Единая ответственность

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

Вот так делать не надо:

const products = [
  {
    name: "CPU",
    price: 228.89,
    bought: true
  },
  {
    name: "RAM",
    price: 67.59,
    bought: false
  },
  {
    name: "HDD",
    price: 70.15,
    bought: true
  },
  {
    name: "Keyboard",
    price: 15.55,
    bought: true
  }
];

function prepareData(items) {
  for (let i = 0; i < items.length; i++) {
    items[i].id = i + 1;
  }

  let boughtItems = [];
  for (let i = 0; i < items.length; i++) {
    if (items[i].bought) {
      boughtItems.push(items[i]);
    }
  }

  let totalCost = 0;
  for (let i = 0; i < boughtItems.length; i++) {
    totalCost += boughtItems[i].price;
  }

  console.log(`Total cost: ${totalCost}$ `);
}

prepareData(products);

Функция prepareData гораздо больше чем должна. Мы спокойно можем разделить её на несколько отдельных функций.

Как делать нужно:

function prepareData(items) {
    addIds(items);
    const boughtItems = getBoughtItems(items);
    let totalCost = computeTotalCost(boughtItems);
    console.log(`Total cost: ${totalCost}$ `);
}

function addIds(items) {
    for (let i = 0; i < items.length; i++) {
        items[i].id = i + 1;
    }
}

function getBoughtItems(items) {
    let boughtItems = [];
    for (let i = 0; i < items.length; i++) {
        if (items[i].bought) {
            boughtItems.push(items[i]);
        }
    }
    return boughtItems;
}

computeTotalCost(items) {
    let totalCost = 0;
    for (let i = 0; i < boughtItems.length; i++) {
        totalCost += boughtItems[i].price;
    }

    return totalCost;
}

prepareData(products);

Уменьшайте количество аргументов

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

Выполняет ли функция конкретную задачу?

Если нет, то Вы должны разделить её на две и более функций.

Но что делать, если Вам всё равно всё еще нужно большее количество аргументов?

Давайте посмотрим на пример.

function draw(element, width, height, backgroundColor, color, margin, padding) {
  element.style.width = width;
  element.style.height = height;
  element.style.backgroundColor = backgroundColor;
  element.style.color = color;
  // Do something;

  return element;
}

Это не очень эффективно. При этом вам нужно помнить порядок аргументов.

Самый лучший вариант здесь запихнуть все аргументы в объект.

function draw(element, config) {
  element.style.width = config.width;
  element.style.height = config.height;
  element.style.backgroundColor = config.backgroundColor;
  element.style.color = config.color;
  // Do something;

  return element;
}

draw(
  document.querySelector("whatever", {
    width: 250,
    height: 300,
    backgroundColor: "red",
    color: "green",
    padding: {
      paddingLeft: 10,
      paddingRight: 15
    }
  })
);

Избегать флагов

Многие разработчики включают флаги в аргументе, для управления выполнением функции. Вот пример.

let toDos = [
  {
    task: "Get some eggs",
    done: false
  },
  {
    task: "Sell pull request",
    done: true
  },
  {
    task: "Watch some videos",
    done: false
  }
];

//filter = 0 prints all toDos
//filter = 1 prints completed toDos
//filter = 2 prints pending toDos

function printToDos(toDos, filter) {
  switch (filter) {
    case 0:
      for (let todo of toDos) {
        console.log(todo.task);
      }
      break;
    case 1:
      for (let todo of toDos) {
        if (todo.done) {
          console.log(todo.task);
        }
      }
      break;
    case 2:
      for (let todo of toDos) {
        if (!todo.done) {
          console.log(todo.task);
        }
      }
      break;
  }
}

Флаг в этой функции управляет функцией.

Но мы можем легко разделить эту функцию на 3 отдельных функции:

function printAllToDos(todos) {
  for (let todo of toDos) {
    console.log(todo.task);
  }
}

function printCompletedToDos(todos) {
  for (let todo of toDos) {
    if (todo.done) {
      console.log(todo.task);
    }
  }
}

function printPendingToDos(todos) {
  for (let todo of toDos) {
    if (!todo.done) {
      console.log(todo.task);
    }
  }
}

Избегайте callbacks (обратных вызовов)

Особенно сейчас, когда есть промисы и async/await функции.

Допустим у нас есть функции которые собирают данные с датчиков. И допустим нам нужно объеденить эти данные, после того как будут получены все параметры.

function getSensorAData(cb) {
  setTimeout(() => {
    cb({
      min: 8,
      max: 118
    });
  }, 2000);
}

function getSensorBData(cb) {
  setTimeout(() => {
    cb({
      min: 78,
      max: 26
    });
  }, 500);
}

function getSensorCData(cb) {
  setTimeout(() => {
    cb({
      min: 14,
      max: 38,
      temp: 65,
      value: 17
    });
  }, 2000);
}

//Callback hell
function combineSensorData() {
  getSensorAData(sensorAData => {
    console.log(`Sensor A Data: ${sensorAData}`);
    getSensorBData(sensorBData => {
      console.log(`Sensor B Data: ${sensorBData}`);
      getSensorCData(sensorCData => {
        console.log(`Sensor C Data: ${sensorCData}`);
      });
    });
  });
}

Давайте посмотрим, насколько код станет чище при использовании promise.

function getSensorAData(cb) {
    return new Promise((resolve, reject) => {setTimeout(() => {
        resolve({
            min:8,
            max: 118
        });
    }, 2000);
    }
} 

//Аналогично для датчика B и C
// ...

function combineSensorData() {
    Promise.all(getSensorAData(), getSensorBData(), getSensorCData())
    .then(results => {
        let [sensorAData, sensorBData, sensorCData] = results;
    })
}

Используйте Object.assign или Spread оператор для аргументов по умолчанию

Например

function draw(element, config) {
    config.width = config.width || 200;
    config.height = config.height || 200;
    config.margin = config.margin || 28;
    config.padding = config.padding || 14;
    console.log(config);
}

draw(null, {margin: 100, padding: 50});

Посмотрим какие бывают проблемы с таким подходом.

Например, если мы укажем нули в качестве аргментов.

draw(null, {margin: 0, height: 0});

В данном случае выведутся значения по умолчанию. А это не то, что нам нужно.

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

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

function draw(element, config) {
    const defaults = {
        width: 200,
        height: 200,
        margin: 28,
        padding: 1
    };

    config = Object.assign(defaults, config);
    console.log(config);
}

draw(null, {margin: 0, padding: 0});

Второй подход: с использованием spread оператора.

function draw(element, config) {
    const defaults = {
        width: 200,
        height: 200,
        margin: 28,
        padding: 1
    };

    config = {
        ...defaults,
        ...config
    };
}

draw(null, {margin: 0, padding: 0});

You might also like More from author

Leave A Reply

Your email address will not be published.