Рефакторинг существующего кода

Код валидации быстро превращается в источник технического долга. Особенно это заметно в проектах, где:

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

Типичный пример проблемного кода:

function validateUser(data) {
    if (!data.email) {
        return 'Email обязателен';
    }

    const emailRegex =
        /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(([^<>()[\]\\.,;:\s@"]+\.)+[^<>()[\]\\.,;:\s@"]{2,})$/i;

    if (!emailRegex.test(data.email)) {
        return 'Некорректный email';
    }

    if (!data.password) {
        return 'Пароль обязателен';
    }

    if (data.password.length < 8) {
        return 'Минимум 8 символов';
    }

    return null;
}

Проблемы такого подхода:

  • сложность поддержки;
  • громоздкие регулярные выражения;
  • дублирование;
  • отсутствие переиспользуемости;
  • плохая читаемость;
  • высокая вероятность ошибок.

Validator.js позволяет сократить объём кода и стандартизировать проверки.


Рефакторинг ручной проверки email

Исходный код

function isValidEmail(email) {
    const regex =
        /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(([^<>()[\]\\.,;:\s@"]+\.)+[^<>()[\]\\.,;:\s@"]{2,})$/i;

    return regex.test(email);
}

Проблемы

Нечитаемость

Регулярное выражение практически невозможно поддерживать.

Риск ошибок

Даже небольшое изменение regex может сломать проверку.

Отсутствие стандартизации

Каждый разработчик может использовать собственный regex.


Рефакторинг с Validator.js

import validator from 'validator';

function isValidEmail(email) {
    return validator.isEmail(email);
}

Результат рефакторинга

Код стал компактнее

Было:

const regex = /^...$/;

Стало:

validator.isEmail(email);

Улучшилась поддерживаемость

Логика проверки скрыта внутри библиотеки.

Повысилась надёжность

Validator.js учитывает множество edge-case сценариев.


Рефакторинг длинных условных конструкций

Исходный код

function validatePassword(password) {
    if (!password) {
        return false;
    }

    if (password.length < 8) {
        return false;
    }

    if (!/[A-Z]/.test(password)) {
        return false;
    }

    if (!/[0-9]/.test(password)) {
        return false;
    }

    return true;
}

Рефакторинг

import validator from 'validator';

function validatePassword(password) {
    return validator.isStrongPassword(password, {
        minLength: 8,
        minUppercase: 1,
        minNumbers: 1
    });
}

Преимущества

Конфигурация вместо логики

Вместо множества условий используется декларативный объект.

Простое изменение правил

minLength: 12

Централизованная настройка

const passwordRules = {
    minLength: 10,
    minUppercase: 1,
    minNumbers: 2,
    minSymbols: 1
};

Рефакторинг повторяющейся валидации

Проблемный код

function registerUser(data) {
    if (!validator.isEmail(data.email)) {
        throw new Error('Invalid email');
    }
}

function updateUser(data) {
    if (!validator.isEmail(data.email)) {
        throw new Error('Invalid email');
    }
}

function resetPassword(data) {
    if (!validator.isEmail(data.email)) {
        throw new Error('Invalid email');
    }
}

Выделение общей логики

import validator from 'validator';

function validateEmail(email) {
    if (!validator.isEmail(email)) {
        throw new Error('Invalid email');
    }
}

Использование

function registerUser(data) {
    validateEmail(data.email);
}

function updateUser(data) {
    validateEmail(data.email);
}

function resetPassword(data) {
    validateEmail(data.email);
}

Создание слоя валидаторов

Организация структуры

validators/
├── emailValidator.js
├── passwordValidator.js
├── userValidator.js
└── index.js

emailValidator.js

import validator from 'validator';

export function validateEmail(email) {
    return validator.isEmail(email);
}

passwordValidator.js

import validator from 'validator';

export function validatePassword(password) {
    return validator.isStrongPassword(password);
}

userValidator.js

import { validateEmail } from './emailValidator.js';
import { validatePassword } from './passwordValidator.js';

export function validateUser(user) {
    return (
        validateEmail(user.email) &&
        validatePassword(user.password)
    );
}

Рефакторинг вложенных проверок

Исходный код

function validateProfile(profile) {
    if (profile) {
        if (profile.email) {
            if (profile.email.length > 5) {
                return validator.isEmail(profile.email);
            }
        }
    }

    return false;
}

Улучшенная версия

function validateProfile(profile) {
    if (!profile?.email) {
        return false;
    }

    return validator.isEmail(profile.email);
}

Дополнительный рефакторинг

function validateProfile(profile) {
    return validator.isEmail(profile?.email || '');
}

Рефакторинг проверки URL

Исходный код

function validateUrl(url) {
    return /^https?:\/\/.+/.test(url);
}

Недостатки

  • неверная обработка сложных URL;
  • отсутствие проверки домена;
  • отсутствие проверки протокола;
  • ложноположительные результаты.

Validator.js

import validator from 'validator';

function validateUrl(url) {
    return validator.isURL(url);
}

Настройка поведения

validator.isURL(url, {
    protocols: ['https'],
    require_protocol: true
});

Рефакторинг очистки данных

Старый подход

function sanitizeInput(text) {
    return text.trim().toLowerCase();
}

Улучшенный вариант

import validator from 'validator';

function sanitizeInput(text) {
    return validator.trim(
        validator.escape(
            validator.toLowerCase(text)
        )
    );
}

Рефакторинг цепочек преобразований

Проблемный код

function normalizeEmail(email) {
    email = email.trim();
    email = email.toLowerCase();

    return email;
}

Validator.js

function normalizeEmail(email) {
    return validator.normalizeEmail(email);
}

Рефакторинг сложной проверки формы

Исходный код

function validateForm(data) {
    const errors = {};

    if (!data.email) {
        errors.email = 'Required';
    } else if (!validator.isEmail(data.email)) {
        errors.email = 'Invalid';
    }

    if (!data.password) {
        errors.password = 'Required';
    } else if (data.password.length < 8) {
        errors.password = 'Too short';
    }

    return errors;
}

Разделение ответственности

Email validator

function validateEmail(email) {
    if (!email) {
        return 'Required';
    }

    if (!validator.isEmail(email)) {
        return 'Invalid';
    }

    return null;
}

Password validator

function validatePassword(password) {
    if (!password) {
        return 'Required';
    }

    if (!validator.isStrongPassword(password)) {
        return 'Weak password';
    }

    return null;
}

Основная функция

function validateForm(data) {
    return {
        email: validateEmail(data.email),
        password: validatePassword(data.password)
    };
}

Рефакторинг через массив правил

Проблемный подход

function validateUsername(username) {
    if (!username) {
        return false;
    }

    if (username.length < 3) {
        return false;
    }

    if (username.length > 20) {
        return false;
    }

    return true;
}

Декларативный стиль

const usernameRules = [
    value => !validator.isEmpty(value),
    value => validator.isLength(value, {
        min: 3,
        max: 20
    })
];

Выполнение правил

function validateUsername(username) {
    return usernameRules.every(rule => rule(username));
}

Рефакторинг с генерацией ошибок

Улучшенная архитектура

const rules = [
    {
        validate: value => validator.isEmail(value),
        message: 'Invalid email'
    },
    {
        validate: value => validator.isLength(value, {
            min: 5
        }),
        message: 'Too short'
    }
];

Проверка

function validate(value) {
    const errors = [];

    for (const rule of rules) {
        if (!rule.validate(value)) {
            errors.push(rule.message);
        }
    }

    return errors;
}

Рефакторинг middleware в Express

Плохой вариант

app.post('/register', (req, res) => {
    const { email } = req.body;

    if (!validator.isEmail(email)) {
        return res.status(400).json({
            error: 'Invalid email'
        });
    }

    res.send('OK');
});

Выделение middleware

function emailValidationMiddleware(req, res, next) {
    if (!validator.isEmail(req.body.email)) {
        return res.status(400).json({
            error: 'Invalid email'
        });
    }

    next();
}

Использование

app.post(
    '/register',
    emailValidationMiddleware,
    controller
);

Рефакторинг с фабриками валидаторов

Универсальная функция

function createLengthValidator(min, max) {
    return value =>
        validator.isLength(value, {
            min,
            max
        });
}

Использование

const validateUsername =
    createLengthValidator(3, 20);

const validateDescription =
    createLengthValidator(10, 500);

Рефакторинг больших validator-функций

Исходный код

function validateUser(user) {
    if (!validator.isEmail(user.email)) {
        return false;
    }

    if (!validator.isStrongPassword(user.password)) {
        return false;
    }

    if (!validator.isLength(user.username, {
        min: 3,
        max: 20
    })) {
        return false;
    }

    if (!validator.isMobilePhone(user.phone)) {
        return false;
    }

    return true;
}

Разделение на небольшие функции

function validateEmail(email) {
    return validator.isEmail(email);
}

function validatePassword(password) {
    return validator.isStrongPassword(password);
}

function validateUsername(username) {
    return validator.isLength(username, {
        min: 3,
        max: 20
    });
}

function validatePhone(phone) {
    return validator.isMobilePhone(phone);
}

Композиция

function validateUser(user) {
    return (
        validateEmail(user.email) &&
        validatePassword(user.password) &&
        validateUsername(user.username) &&
        validatePhone(user.phone)
    );
}

Рефакторинг через объект схемы

Схема валидации

const schema = {
    email: value => validator.isEmail(value),

    username: value =>
        validator.isLength(value, {
            min: 3,
            max: 20
        }),

    password: value =>
        validator.isStrongPassword(value)
};

Универсальный валидатор

function validate(data, schema) {
    const errors = {};

    for (const field in schema) {
        const isValid =
            schema[field](data[field]);

        if (!isValid) {
            errors[field] = 'Invalid value';
        }
    }

    return errors;
}

Рефакторинг проверки чисел

Исходный код

function validateAge(age) {
    const num = Number(age);

    if (isNaN(num)) {
        return false;
    }

    if (num < 18) {
        return false;
    }

    return true;
}

Validator.js

function validateAge(age) {
    return validator.isInt(age, {
        min: 18
    });
}

Рефакторинг проверки JSON

Ручная реализация

function isValidJson(value) {
    try {
        JSON.parse(value);

        return true;
    } catch {
        return false;
    }
}

Validator.js

function isValidJson(value) {
    return validator.isJSON(value);
}

Рефакторинг обработки ошибок

Проблемный код

if (!validator.isEmail(email)) {
    console.log('Email error');
}

if (!validator.isURL(site)) {
    console.log('URL error');
}

Централизованная обработка

function validateField(value, validatorFn, message) {
    if (!validatorFn(value)) {
        throw new Error(message);
    }
}

Использование

validateField(
    email,
    validator.isEmail,
    'Invalid email'
);

validateField(
    site,
    validator.isURL,
    'Invalid URL'
);

Рефакторинг legacy-кода

Типичная проблема старых проектов

function validate(data) {
    if (data.email.indexOf('@') === -1) {
        return false;
    }

    if (data.name.length < 2) {
        return false;
    }

    return true;
}

Постепенная миграция

Этап 1 — замена простых проверок

function validate(data) {
    if (!validator.isEmail(data.email)) {
        return false;
    }

    if (!validator.isLength(data.name, {
        min: 2
    })) {
        return false;
    }

    return true;
}

Этап 2 — выделение функций

function validateEmail(email) {
    return validator.isEmail(email);
}

function validateName(name) {
    return validator.isLength(name, {
        min: 2
    });
}

Этап 3 — создание слоя validation

src/
├── validators/
├── schemas/
├── middleware/
└── utils/

Антипаттерны при рефакторинге

Чрезмерная абстракция

Плохо:

const executeValidationStrategyFactory =
    createValidationExecutorFactory();

Хорошо:

validateEmail(email);

Огромные универсальные валидаторы

Плохо:

validateEverything(data);

Лучше:

validateUser(user);
validateProduct(product);
validateOrder(order);

Смешивание валидации и бизнес-логики

Плохо:

function register(user) {
    if (!validator.isEmail(user.email)) {
        return;
    }

    saveUser(user);
}

Лучше:

validateUser(user);
saveUser(user);

Стратегии безопасного рефакторинга

Маленькие изменения

Нежелательно переписывать всю систему валидации сразу.

Лучше:

  1. заменить regex;
  2. выделить функции;
  3. убрать дублирование;
  4. внедрить схемы;
  5. стандартизировать ошибки.

Покрытие тестами

Перед рефакторингом:

describe('validateEmail', () => {
    test('валидный email', () => {
        expect(
            validateEmail('test@mail.com')
        ).toBe(true);
    });
});

Сохранение поведения

Рефакторинг не должен менять бизнес-логику.

Изменяется:

  • структура;
  • читаемость;
  • поддерживаемость.

Не изменяется:

  • поведение;
  • контракт функций;
  • формат ошибок.

Метрики успешного рефакторинга

Снижение количества кода

Было:

if (!value || value.length < 5)

Стало:

validator.isLength(value, { min: 5 })

Уменьшение дублирования

Общая логика выносится в отдельные функции.


Повышение читаемости

Было:

/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i

Стало:

validator.isEmail(email)

Улучшение тестируемости

Небольшие функции проще покрывать тестами.

test('validatePassword', () => {
    expect(
        validatePassword('Qwerty123')
    ).toBe(true);
});

Практический пример полного рефакторинга

До рефакторинга

function validateUser(data) {
    const errors = {};

    if (
        !data.email ||
        !/^.+@.+\..+$/.test(data.email)
    ) {
        errors.email = 'Invalid email';
    }

    if (
        !data.password ||
        data.password.length < 8
    ) {
        errors.password = 'Invalid password';
    }

    if (
        !data.username ||
        data.username.length < 3
    ) {
        errors.username = 'Invalid username';
    }

    return errors;
}

После рефакторинга

validators/email.js

export function validateEmail(email) {
    return validator.isEmail(email || '');
}

validators/password.js

export function validatePassword(password) {
    return validator.isStrongPassword(password || '', {
        minLength: 8,
        minUppercase: 0,
        minSymbols: 0
    });
}

validators/username.js

export function validateUsername(username) {
    return validator.isLength(username || '', {
        min: 3
    });
}

validators/user.js

import { validateEmail } from './email.js';
import { validatePassword } from './password.js';
import { validateUsername } from './username.js';

export function validateUser(data) {
    const errors = {};

    if (!validateEmail(data.email)) {
        errors.email = 'Invalid email';
    }

    if (!validatePassword(data.password)) {
        errors.password = 'Invalid password';
    }

    if (!validateUsername(data.username)) {
        errors.username = 'Invalid username';
    }

    return errors;
}