4 принципа качественного рефакторинга функций.
Каждый проект в программировании так или иначе связан с данными, в управлении которыми важную роль играют функции, подготавливающие эти данные для представления в числовом или графическом формате. Вот почему любому программисту необходимо научиться правильно их писать.
Новички ошибочно полагают, что их более опытные коллеги-программисты с первого раза пишут идеальные функции. Возможно, с простыми из них так и получается, но чаще в
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 основных принципа, которыми можно руководствоваться в процессе преобразования кода. Пусть вас не смущают ошибки. Именно совершая и исправляя их, вы совершенствуете свои навыки программирования. Весь процесс работы над проектом неразрывно связан с рефакторингом.
Формирование навыков программирования требует времени. Терпение и еще раз терпение.