4 принципа качественного рефакторинга функций

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

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

Если вы знаете, как проводить данную процедуру, и при этом для вас не секрет, что из себя представляет правильная функция, то вы достойны звания опытного программиста. В этой статье я поделюсь 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

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 protected]", "12345678");
user.phoneNumber = "1234567890";
user.address = "123 Main Street";
createNewUserAccount(user);
  • Теперь у нас есть класс User, который вместо применения словаря перехватывает относящиеся к пользователю данных.
  • Какие-то необязательные данные, например phone number, остаются за рамками конструктора, что обеспечивает дополнительную гибкость при создании user и уменьшает шум конструктора. 

Заключение 

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

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

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

Читайте нас в Telegram, VK и Яндекс.Дзен


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