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

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

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

1. Избегайте дублирования 

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

function sayHello(name) {
    // Применение операций форматирования 
    let name0 = name;
    let name1 = name0;
    let name2 = name1;
    let formattedName = name2;
    console.log("Hello,", formattedName);
}

function sayHi(name) {
    // Применение операций форматирования 
    let name0 = name;
    let name1 = name0;
    let name2 = name1;
    let formattedName = name2;
    console.log("Hi,", formattedName);
}

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

function sayHelloV1(name) {
    let formattedName = formatName(name);
    console.log("Hello,", formattedName);
}

function sayHiV1(name) {
    let formattedName = formatName(name);
    console.log("Hi,", formattedName);
}

function formatName(name) {
    // Применение операций форматирования 
    let name0 = name;
    let name1 = name0;
    let name2 = name1;
    return name2;
}

В этой версии мы избавляемся от большей части повторяющегося кода. Однако нельзя не заметить, что некоторое повторение сохранилось. Я говорю про вывод sayHelloV1 и sayHiV1, которые используют слегка отличные строки в качестве приветственного слова (greetingWord). Исправим это упущение в следующей версии и получим более удачный вариант: 

function greet(greetingWord, name) {
    let formattedName = formatName(name);
    console.log(greetingWord + ", " + formattedName);
}

Как следует из примера, мы объединили sayHello и sayHi за счет абстрагирования всех их общих компонентов. А главное, получившаяся функция приобрела более обобщенный вид, позволяя пользователю применить отличное от Hi или Hello приветствие, например Good morning. 

Telegram-канал по теме
NOP::HumorИТ-юмор и мемы о боли разработчиков
Подписаться

2. Сохраняйте компактный размер функций 

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

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

function processData(filename) {
    // 3 строки кода: считывание данных из имени файла 
    // 5 строк кода: проверка структуры данных  
    // 10 строк кода: перекодировка некоторых столбцов
    // 5 строк кода: удаление выбросов 
    // 10 строк кода: выполнение вычислений с группами 
    let results = [];
    return results;
}

Данный пример не отображает фактический код, но позволяет получить представление о потенциально сложной функции. Хотя в ней и нет повторяющегося кода, прочитать ее будет нелегко. При отсутствии комментариев к отдельным компонентам функции затруднительно понять назначение каждого из них. Более того, если в работе программы возникнут сбои, то процесс отладки, вероятно, усложнится. Рассмотрим обновленную версию после рефакторинга: 

function processDataRefactored(filename) {
    let data = readData(filename);
    verifyData(data);
    recodeData(data);
    removeOutliers(data);
    let processedData = summarizeData(data);
    return processedData;
}

function readData(filename) {
    // Простой список для наглядности 
    let data = [1, 2, 3, 4];
    return data;
}

function verifyData(data) {
    // перекодировка данных 
}

function recodeData(data) {
    // запись данных 
}

function removeOutliers(data) {
    // удаление выбросов 
}

function summarizeData(data) {
    let results = [];
    return results;
}

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

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

3. Давайте функциям содержательные имена 

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

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

// Эта функция получает данные учетной записи для пользователя по его id 
function getData(id) {
    // операции 
}

Эта функция таит в себе две проблемы. Во-первых, непонятно, какие данные она получает, а во-вторых, что означает параметр id. Интересно, что программист стремится все объяснить в комментарии, тем самым допуская ошибку, свойственную многим новичкам: пытается при помощи пояснений частично дополнить код. Однако в большинстве случаев в этом нет необходимости. Рассмотрим следующую преобразованную версию: 

function getUserAccountInfo(userIdNumber) {
    // операции
}
  • Имя функции четко отражает ее назначение. 
  • У параметров также должны быть содержательные имена. 

В этом разделе речь идет только о содержательных именах одной функции. Однако таким образом следует именовать все функции и учитывать следующее: 

  • Функции с похожими задачами должные иметь и похожие имена. Например, если набор функций получает (get) какие-то данные, то все их имена могут начинаться с get: getUserAccountInfo и getUserFriendList.
  • Не боитесь давать длинное имя. Современные IDE предоставляют возможность автозаполнения. Вам будет предложен динамический список, в котором по первым буквам можно будет найти имя созданной функции. Помните о том, что содержательное длинное имя всегда предпочтительнее короткого, но непонятного.  

4. Минимизируйте число параметров 

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

function createNewUserAccount(username, email, password, phoneNumber, address) {
    let user = {
        "username": username,
        "email": email,
        "password": password,
        "phoneNumber": phoneNumber,
        "address": address
    }
    // создание нового пользователя в базе данных 
}

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

function createNewUserAccount(newUserInfoDict) {
    // создание нового пользователя в базе данных 
}

let userInfo = {
    "username": username,
    "email": email,
    "password": password,
    "phoneNumber": phoneNumber,
    "address": address
}
createNewUserAccount(userInfo)

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

class User {
    constructor(username, email, password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }
}

function createNewUserAccount(newUser) {
    // создание нового пользователя в базе данных 
}

let user = new User("username", "email@email.com", "12345678");
user.phoneNumber = "1234567890";
user.address = "123 Main Street";
createNewUserAccount(user);
  • Теперь у нас есть класс User, который вместо применения словаря перехватывает относящиеся к пользователю данных.
  • Какие-то необязательные данные, например phone number, остаются за рамками конструктора, что обеспечивает дополнительную гибкость при создании user и уменьшает шум конструктора. 

Заключение 

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

Формирование навыков программирования требует времени. Терпение и еще раз терпение. 

Читайте также:


Перевод статьи Yong Cui, Ph.D.: 4 Principles When Refactoring Your Functions

Мы в Telegram — подписывайтесь
NOP::Nuances of ProgrammingСтатьи и переводы для разработчиков — каждый день
Подписаться
NOP::HumorИТ-юмор и мемы о боли разработчиков
Подписаться
NOP::Recruiter RemoteУдалённые ИТ-вакансии с вилками
Подписаться
Предыдущая статьяТри функции JavaScript для освоения метода Reduce
Следующая статья10 самых продуктивных техник для работы с файлами в Python