Каждый проект в программировании так или иначе связан с данными, в управлении которыми важную роль играют функции, подготавливающие эти данные для представления в числовом или графическом формате. Вот почему любому программисту необходимо научиться правильно их писать.
Новички ошибочно полагают, что их более опытные коллеги-программисты с первого раза пишут идеальные функции. Возможно, с простыми из них так и получается, но чаще всего в реальных проектах приходится писать много пробных версий, прежде чем достичь удовлетворительного результата. Такой процесс преобразования обычно называется рефакторингом функций.
Если вы знаете, как проводить данную процедуру, и при этом для вас не секрет, что из себя представляет правильная функция, то вы достойны звания опытного программиста. В этой статье я поделюсь 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 основных принципа, которыми можно руководствоваться в процессе преобразования кода. Пусть вас не смущают ошибки. Именно совершая и исправляя их, вы совершенствуете свои навыки программирования. Весь процесс работы над проектом неразрывно связан с рефакторингом.
Формирование навыков программирования требует времени. Терпение и еще раз терпение.
Читайте также:
- Рефакторинг: от мусорного кода к SOLID-ному
- P.S. Дорогой рефакторинг, нам нужно на время расстаться
- Рефакторинг большой раскадровки в несколько меньших
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Yong Cui, Ph.D.: 4 Principles When Refactoring Your Functions