Программирование — это искусство находить золотую середину между стремлением к идеальному коду и реальными ограничениями времени.
Нельзя написать руководство, которое покрывало бы все вопросы написания кода, но мы постараемся обсудить общие рекомендации направленные на улучшение качества вашего кода.
Наименования
Имена переменных должны описывать, что хранится в переменной или в свойстве, что функция или метод делают.
Переменные и свойства
Переменные и свойства содержат данные - числа, текст, логические значения, объекты, массивы, мапы. Поэтому наименование должно обозначать какого рода данные хранятся
Таким образом, переменные и свойства, как правило, должны получать имена в виде существительных. Например: user
, product
, customer
, database
, transaction
и т.д.
В качестве альтернативы можно использовать короткие фразы с прилагательным — обычно для хранения булевых значений. Например: isValid
, didAuthenticate
, isLoggedIn
, emailExists
и т.п.
Как правило, если можно быть более конкретным, стоит быть более конкретным.
Например, предпочтительнее использовать customer
, а не user
, если код выполняет операции, специфичные для клиента. Это делает ваш код более понятным и удобным для чтения.
Функции и методы
Функции и методы, как правило, должны получать имена в виде глаголов. Например: login()
, createUser()
, database.insert()
, log()
и т.д.
В качестве альтернативы, функции и методы могут использоваться для получения значений — в таком случае, особенно если возвращается булево значение, можно использовать короткие фразы с прилагательными. Например: isValid(...)
, isEmail(...)
, isEmpty(...)
и т.п.
Следует избегать имён, таких как email()
, user()
и т.д. Эти имена звучат как свойства. Вместо этого предпочтительнее использовать, например, getEmail()
.
Как и в случае с переменными и свойствами, если можно быть более конкретным, обычно имеет смысл использовать такие имена. Например: createUser()
вместо просто create()
.
Классы
Имя класса должно описывать тип объекта, который он создаёт.
Хорошие имена классов, как и хорошие имена переменных и свойств, — это существительные. Например: User
, Product
, RootAdministrator
, Transaction
, Payment
и т.д.
Избегайте общих имён
В большинстве ситуаций следует избегать общих имён, таких как handle()
, process()
, data
, item
и т.п.
Всегда могут быть ситуации, где такие имена уместны, но обычно стоит либо сделать их более конкретными (например, processTransaction()
), либо выбрать другой тип имени (например, product
вместо item
).
Будьте последовательны
Важная часть использования правильных имён — это последовательность.
Если вы использовали fetchUsers()
в одной части кода, то в другой части того же кода следует использовать fetchProducts()
— а не getProducts()
.
В целом, не важно, предпочитаете ли вы fetch...(), get...(), retrieve...()
или любой другой термин, но вы должны быть последовательны.
Комментарии и форматирование
Можно подумать, что комментарии помогают улучшить читаемость кода. Однако на практике часто всё наоборот.
С другой стороны, правильное форматирование кода (например, короткие строки, добавление пустых строк и т.д.) действительно помогает в чтении и понимании кода.
Закомментированный код
Старайтесь избегать закомментированного кода. Вместо этого просто удалите его.
При использовании системы контроля версий (например, Git) вы всегда можете вернуть старый код, если он понадобится.
Избыточная информация
Комментарии, как в этом примере, не несут никакой пользы. Вместо этого вы останавливаетесь и тратите время на их чтение — только чтобы узнать то, что и так было понятно из правильно названного кода.
function createUser() { // создание нового пользователя
...
}
Такой комментарий мог бы быть полезен, если бы имена были плохими:
function x() { // создание нового пользователя
...
}
Но, конечно, изначально следует избегать плохих имён.
Вводящие в заблуждение комментарии
Пожалуй, худший вид комментариев — это те, которые вводят читателя в заблуждение.
function login() { // создание нового пользователя
...
}
Эта функция выполняет вход пользователя (как следует из её имени) или создаёт нового пользователя (как указано в комментарии)?
Мы не знаем — и теперь нам нужно проанализировать всю функцию (и любые другие функции, которые она может вызывать), чтобы это выяснить.
Хорошие комментарии
Хотя комментарии не улучшают ваш код, есть несколько типов комментариев, которые могут быть полезны.
"Необходимые" пояснения
В редких случаях добавление дополнительных пояснений к коду действительно помогает — даже если всё названо правильно.
Хороший пример — регулярные выражения:
// Минимум 8 символов, включая: одну букву, одну цифру, один специальный символ
const passwordRegex = /^(?=.*[A-Za-z])(?=.*d)(?=.*[@$!%*#?&])[A-Za-zd@$!%*#?&]{8,}$/
Хотя имя passwordRegex
говорит нам, что это регулярное выражение используется для проверки паролей, не сразу понятно, какие именно правила применяются.
Регулярные выражения не так легко читать, поэтому добавление такого комментария не помешает.
Предупреждения
Также в редких случаях предупреждения рядом с кодом могут быть полезны — например, если модульный тест может выполняться долго или если определённая функциональность не работает в некоторых средах.
function fetchTestData() { ... } // требует локального dev-сервера
Заметки "Todo"
Хотя не стоит злоупотреблять этим, добавление заметок "Todo" тоже может быть допустимо.
Конечно, лучше полностью реализовать функциональность или не реализовывать её вовсе — или делать это поэтапно, без необходимости в комментариях "Todo". Но оставить пару таких комментариев не повредит, особенно поскольку современные IDE помогают их находить.
// TODO: Реализовать проверку данных
Вертикальное форматирование
Вертикальное форматирование связано с использованием вертикального пространства в вашем файле с кодом. Речь идёт о добавлении пустых строк, а также о группировке связанных концепций и разделении несвязанных.
Добавление пустых строк
Рассмотрим этот пример:
function login(email, password) {
if (!email.includes('@') || password.length < 7) {
throw new Error('Invalid input!');
}
const user = findUserByEmail(email);
const passwordIsValid = compareEncryptedPassword(user.password, password);
if (passwordIsValid) {
createSession();
} else {
throw new Error('Invalid credentials!');
}
}
function signup(email, password) {
if (!email.includes('@') || password.length < 7) {
throw new Error('Invalid input!');
}
const user = new User(email, password);
user.saveToDatabase();
}
Этот код использует хорошие имена и не слишком длинный, но его всё равно может быть сложно воспринимать. Сравните с этим вариантом:
function login(email, password) {
if (!email.includes('@') || password.length < 7) {
throw new Error('Invalid input!');
}
const user = findUserByEmail(email);
const passwordIsValid = compareEncryptedPassword(user.password, password);
if (passwordIsValid) {
createSession();
} else {
throw new Error('Invalid credentials!');
}
}
function signup(email, password) {
if (!email.includes('@') || password.length < 7) {
throw new Error('Invalid input!');
}
const user = new User(email, password);
user.saveToDatabase();
}
Это тот же самый код, но дополнительные пустые строки улучшают читаемость.
Таким образом, вы должны добавлять вертикальные отступы, чтобы сделать код чище.
Cвязанные концепции должны находиться близко друг к другу.
Концепции, которые не связаны между собой, должны быть разделены.
Это касается как отдельных операций внутри функции, так и функций/методов в целом.
Вот пример:
function signup(email, password) {
if (!email.includes('@') || password.length < 7) {
throw new Error('Invalid input!');
}
const user = new User(email, password);
user.saveToDatabase();
}
Здесь в функции есть две основные концепции: валидация и создание нового пользователя в базе данных.
Эти концепции должны быть разделены пустой строкой.
С другой стороны, создание объекта пользователя и вызов метода saveToDatabase() тесно связаны, поэтому между ними не должно быть пустой строки.
Порядок функций и методов
При упорядочивании функций и методов рекомендуется следовать правилу «сверху вниз».
Функция A, которая вызывается функцией B, должна находиться (близко) под функцией B — по крайней мере, если ваш язык программирования позволяет такое упорядочивание.
function login(email, password) {
validate(email, passsword);
...
}
function validate(email, password) {...}
Разделение кода на файлы
Если ваш файл с кодом становится слишком большим и/или содержит много разных "сущностей" (например, несколько определений классов), считается хорошей практикой разделить этот код на несколько файлов и использовать операторы import
и export
для связи кода. Это гарантирует, что отдельные файлы с кодом остаются читаемыми.
Горизонтальное форматирование
В современной разработке за это отвечают различные форматтеры кода prettier, biome
т.д.
Строки кода должны быть относительно короткими.
Стоит избегать чрезмерной специфичности в наименованиях
const loggedInUserAuthenticatedByEmailAndPassword = ...
const loggedInUser = ....
Функции и методы
Минимизируйте количество параметров
Чем меньше параметров у функции, тем проще её читать и вызывать (а также легче читать и понимать выражения, в которых функция вызывается).
Рассмотрим пример:
createUser('Max', 'Max', 'test@test.com', 'testers', 31, ['Sports', 'Cooking']);
Вызов такой функции не доставляет удовольствия. Нужно помнить, какие параметры обязательны и в каком порядке нужно передавать.
Чтение такого кода тоже не радует — например, не сразу понятно, почему у нас два значения 'Max'
в списке.
Сколько параметров допустимо?
В целом, чем меньше, тем лучше.
Функции без параметров, конечно, очень легко читать и понимать. Например:
createSession();
user.save();
Но отсутствие параметров — не всегда вариант. Ведь именно возможность принимать параметры делает функции динамичными и гибкими.
К счастью, функции с одним параметром тоже просты:
log(message);
Функции с двумя параметрами могут быть допустимы — всё зависит от контекста и типа функции.
Например, такой код должен быть простым и понятным:
add(5, 10);
С другой стороны, могут встретиться функции, где даже два параметра уже вызывают путаницу, и не очевидно, какое значение куда передавать:
calculate(discount, price);
Конечно, современные IDE помогают понять, какие значения и в каком порядке ожидаются, но необходимость наводить курсор на эти функции — это дополнительный шаг, который ухудшает читаемость кода.
Функции с более чем двумя параметрами по возможности следует избегать — такие функции могут быть сложными для вызова и чтения:
updateUserProfile('Max', 'test@test.com', 31, ['Sports', 'Cooking'], true);
Как уменьшить количество параметров?
Что делать, если функция принимает слишком много параметров, но все эти данные ей нужны?
Вы можете заменить несколько параметров на объект или массив!
// Вместо этого:
createUser('Max', 'Max', 'test@test.com', 'testers', 31, ['Sports', 'Cooking']);
// Используйте это:
const userData = {
firstName: 'Max',
lastName: 'Max',
email: 'test@test.com',
role: 'testers',
age: 31,
hobbies: ['Sports', 'Cooking']
};
createUser(userData);
Пишите небольшие функции
Рассмотрим этот пример:
function login(email, password) {
if (!email.includes('@') || password.length < 7) {
throw new Error('Invalid input!');
}
const existingUser = database.find('users', 'email', '==', email);
if (!existingUser) {
throw new Error('Could not find a user for the provided email.');
}
if (existingUser.password === password) {
// create a session
} else {
throw new Error('Invalid credentials!');
}
}
Если вы прочитаете этот фрагмент, вы, вероятно, довольно быстро поймете, что он делает. Потому что это короткий и простой фрагмент.
Теперь рассмотрим этот фрагмент, который делает то же самое:
function login(email, password) {
validateUserInput(email, password);
const existingUser = findUserByEmail(email);
existingUser.validatePassword(password);
}
Он гораздо корочеи проще для восприятия, не так ли?
И это цель! Иметь короткие, сфокусированные функции, которые легко читать, понимать и поддерживать.
Одно действие
Чтобы функция была небольшой, она должна выполнять только одно действие.
Это гарантирует, что функция не делает слишком много.
Что такое «одно действие»?
Рассмотрим эту функцию:
function login(email, password) {
validateUserInput(email, password);
verifyCredentials(email, password);
createSession();
}
Выполняет ли эта функция одну задачу?
Можно утверждать, что она делает три вещи:
-
Проверяет ввод пользователя.
-
Проверяет учетные данные.
-
Создает сессию.
Однако, идея функции, которая выполняет одно действие, связана с другим понятием: уровни абстракции различных операций в функции.
Функция считается выполняющей одно действие, если все операции в теле функции находятся на одном уровне абстракции и на один уровень ниже, чем название функции.
Уровни абстракции
Уровни абстракции могут быть запутанными, но в конечном итоге это довольно простая концепция.
В программировании существуют высокоуровневые и низкоуровневые операции — и между этими двумя крайностями большая разница.
Рассмотрим пример:
Вызов db.connect()
— это высокоуровневая операция. Мы не работаем с внутренностями языка программирования, мы не устанавливаем сетевое соединение в деталях. Мы просто вызываем функцию, которая затем выполняет множество действий "под капотом".
function connectToDatabase(uri) {
if (uri === '') {
console.log('Invalid URI!');
return;
}
const db = new Database(uri);
db.connect();
}
С другой стороны, console.log(...)
, как и сравнение uri === ''
, — это низкоуровневая операция. Высокоуровневый эквивалент мог бы выглядеть так:
if (!uriIsValid(uri)) {
showError('Invalid URI!');
return;
}
Теперь детали реализации "скрыты" (абстрагированы).
Низкие уровни абстракции — это не плохо! Однако их не следует смешивать с высокоуровневыми операциями, так как это может вызвать путаницу и сделать код сложнее для понимания.
Старайтесь писать функции, в которых все операции находятся на одном уровне абстракции, и этот уровень должен быть ровно на один уровень ниже, чем уровень, подразумеваемый именем функции
Понимание этих концепций — важный шаг к написанию чистых функций.
Операции должны быть на один уровень ниже имени функции
Рассмотрим функцию login
:
function login(email, password) {
validateUserInput(email, password);
verifyCredentials(email, password);
createSession();
}
Все три операции находятся на одном уровне абстракции (довольно высоком в данном случае) и на один уровень ниже, чем уровень, подразумеваемый именем функции.
Конечно, границы здесь размыты.
Что насчёт этого слегка изменённого примера?
function login(email, password) {
if (inputInvalid(email, password)) {
showError(email, password);
return;
}
verifyCredentials(email, password);
createSession();
}
Здесь мы по-прежнему имеем относительно высокий уровень абстракции, но можно поспорить, находятся ли все операции на одном уровне. verifyCredentials(...)
кажется более высокоуровневой операцией, чем ручная проверка с помощью if
и вывод ошибки
Кроме того, хотя проверка ввода относится к задачам, выполняемым функцией login()
, можно задаться вопросом, стоит ли вызывать showError(...)
напрямую внутри login()
. Это кажется более чем на один уровень ниже, чем инструкция login()
.
Очевидно, всегда есть место для обсуждения и интерпретации.
И более высокая детализация не всегда лучше
Избегайте смешанных уровней абстракции
Как упоминалось выше, уровни абстракции не следует смешивать, так как это снижает читаемость и может вызвать путаницу.
Рассмотрим пример:
function printDocument(documentPath) {
const fsConfig = { mode: 'read', onError: 'retry' };
const document = fileSystem.readFile(documentPath, fsConfig);
const printer = new Printer('pdf');
printer.print(document);
}
Этот код не слишком объёмный, но он смешивает уровни абстракции. Настройка операции readFile()
и выполнение всех этих отдельных шагов рядом с довольно высокоуровневыми операциями печати добавляет ненужную сложность в эту функцию.
Вот более чистый вариант:
function printDocument(documentPath) {
const document = readFromFile(documentPath);
const printer = new Printer('pdf');
printer.print(document);
}
Здесь readFromFile()
может позаботиться о точных шагах, которые необходимо выполнить для чтения документа.
Конечно, можно возразить, что это можно разделить ещё больше:
function printDocument(documentPath) {
const document = readFromFile(documentPath);
printFile(document);
}
Но новая функция printFile()
практически просто перефразирует функцию printDocument
. Так что такое разделение возможно, но оно не всегда будет удачным решением
Практические правила
Концепция "уровней абстракции" может показаться сложной, и вам точно не стоит тратить часы на анализ кода, чтобы понять, какие уровни абстракции в нём присутствуют.
Вместо этого я предлагаю два простых практических правила, которые помогут вам решить, когда стоит разделять код:
-
Выделяйте код, который работает над одной функциональностью или тесно связан.
-
Выделяйте код, который требует большего осмысления, чем окружающий его код.
Вот пример для правила №1:
function updateUser(userData) {
validateUserData(userData);
const user = findUserById(userData.id);
user.setAge(userData.age);
user.setName(userData.name);
user.save();
}
Методы setAge()
и setName()
преследуют одну цель / функциональность: они обновляют данные в объекте пользователя. Метод save()
затем подтверждает эти изменения.
Вы можете разделить эту функцию:
function updateUser(userData) {
validateUserData(userData);
applyUpdate(userData);
}
function applyUpdate(userData) {
const user = findUserById(userData.id);
user.setAge(userData.age);
user.setName(userData.name);
user.save();
}
Просто следуя этому правилу, вы неявно устранили ещё одну проблему: смешение уровней абстракции в исходной функции.
Вот пример для правила №2:
function processTransaction(transaction) {
if (transaction.type === 'UNKNOWN') {
throw new Error('Invalid transaction type.');
}
if (transaction.type === 'PAYMENT') {
processPayment(transaction);
}
}
Проверка на то, является ли тип транзакции 'UNKNOWN'
, конечно, не сложна для понимания, но она определённо требует большего осмысления, чем просто чтение processPayment(...)
.
Таким образом, вы можете рефакторить это:
function processTransaction(transaction) {
validateTransaction(transaction);
if (isPayment(transaction)) {
processPayment(transaction);
}
}
Теперь код стал очень читаемым, и ни один шаг не требует дополнительного осмысления со стороны читателя.
Разделяйте функции разумно
Со всеми этими правилами, и поскольку вы, конечно, точно не хотите писать плохой код, вы можете привыкнуть выделять всё в новые функции.
Это опасно — потому что это тоже может привести к плохому коду.
Рассмотрим пример:
function createUser(email, password) {
validateInput(email, password);
saveUser(email, password);
}
function validateInput(email, password) {
if (!isEmail(email) || isInvalidPassword(password)) {
throwError('Invalid input');
}
}
function isEmail(email) { ... }
function isInvalidPassword(password) { ... }
function throwError(message) {
throw new Error(message);
}
function saveUser(email, password) {
const user = buildUser(email, password);
user.save();
}
function buildUser(email, password) {
return new User(email, password);
}
А теперь сравните его с этой версией:
function createUser(email, password) {
validateInput(email, password);
saveUser(email, password);
}
function validateInput(email, password) {
if (!isEmail(email) || isInvalidPassword(password)) {
throw new Error('Invalid input');
}
}
function isEmail(email) { ... }
function isInvalidPassword(password) { ...
function saveUser(email, password) {
const user = new User(email, password);
user.save();
}
Какая версия легче для понимания?
Я бы сказал, что вторая. Даже (или скорее именно потому), что в ней меньше выделенных функций.
Разделение функций и поддержание их короткими — это важно! Но бессмысленное выделение ни к чему не приводит — не стоит выделять функции просто ради самого выделения.
Как понять, что выделение не имеет смысла?
Есть три основных сигнала:
-
Вы просто переименовываете операцию.
Например, функцияthrowError()
просто переименовывает операцию throw newError(...)
. -
Вам внезапно приходится прокручивать намного больше, чтобы следить за логикой простой функции.
Если код становится сложнее читать из-за излишнего разделения, это плохой знак. -
Вы не можете придумать разумное имя для выделенной функции, которое ещё не занято.
Например, в случае сbuildUser()
было сложно придумать подходящее имя, потому чтоcreateUser()
уже занято — и делает больше, чем просто создание объекта пользователя.
В примере выше функции throwError()
и buildUser()
в конечном итоге просто переименовывали операции, которые они содержали. Для buildUser()
придумать хорошее имя было сложно, потому что createUser()
уже занято — и выполняет больше действий, чем просто создание объекта пользователя.
Избегайте неожиданных побочных эффектов
Побочный эффект — это операция, которая изменяет состояние (данные, статус системы и т.д.) приложения.
Примеры побочных эффектов:
-
Подключение к базе данных.
-
Отправка HTTP-запроса.
-
Вывод данных в консоль.
-
Изменение данных, сохранённых в памяти.
Побочные эффекты — это нормальная часть разработки.
Проблемы возникают, когда побочный эффект оказывается неожиданным.
Побочный эффект считается неожиданным, если имя или контекст функции не подразумевают его наличие.
Рассмотрим пример:
function validateUserInput(email, password) {
if (!isEmail(email) || passwordIsInvalid(password)) {
throw new Error('Invalid input!');
}
createSession();
}
Эта функция имеет неожиданный побочный эффект: вызов createSession().
Создание сессии (что влияет на данные в памяти, а возможно, даже на данные в базе данных или файлах) — это определённо побочный эффект.
Мы могли бы ожидать такой побочный эффект в функции с именем login()
, но в функции validateUserInput()
он точно не ожидается. И это проблема.
Следовательно, вам следует переместить этот побочный эффект в другую функцию или, если это имеет смысл, переименовать функцию, чтобы подразумевать, что этот побочный эффект будет вызван.
Директивы
Независимо от того, какое приложение вы создаёте, вы, скорее всего, будете использовать директивы в своём коде: операторы if
, циклы for
, а возможно, и циклы while
, или операторы switch-case
.
Управляющие структуры крайне важны для координации потока выполнения кода, и, конечно, их следует использовать.
Однако управляющие структуры также могут привести к плохому или неоптимальному коду, поэтому они играют важную роль в написании чистого кода.
Положительные проверки
Это довольно просто. Иногда имеет смысл использовать позитивные формулировки в ваших проверках if
, а не негативные.
На мой взгляд, иногда короткая негативная формулировка лучше, чем искусственно созданная позитивная.
Рассмотрим пример:
if (isEmpty(blogContent)) {
// выбросить ошибку
}
if (!hasContent(blogContent)) {
// выбросить ошибку
}
Первый фрагмент довольно читаем и не требует дополнительных размышлений.
Второй фрагмент использует оператор ! для проверки на противоположное — читателю требуется немного больше усилий для интерпретации.
Следовательно, вариант №1 предпочтительнее.
Однако иногда я предпочитаю негативную версию:
if (!isOpen(transaction)) {
// выбросить ошибку
}
if (isClosed(transaction)) {
// выбросить ошибку
}
На первый взгляд кажется, что вариант №2 лучше.
И в целом так может быть. Но что, если у нас не только "Открытые" и "Закрытые" транзакции? Что, если есть ещё и "Неизвестные"?
if (!isOpen(transaction)) {
// выбросить ошибку
}
if (isClosed(transaction) || isUnknown(transaction)) {
// выбросить ошибку
}
Это быстро усложняется! Чем больше возможных вариантов, тем больше проверок нужно комбинировать.
Или мы просто проверяем на противоположное — в данном примере, просто на то, что транзакция НЕ открыта.
Избегайте глубокой вложенности
Это очень важно! Вам следует избегать глубоко вложенных управляющих структур, так как такой код плохо читаем, сложен в поддержке и часто подвержен ошибкам.
Есть несколько техник, которые помогут избавиться от глубокой вложенности и дублирования кода:
-
Используйте "защитные условия" (guards) и "быстрый выход" (fail fast).
-
Выносите директивы и логику в отдельные функции.
-
Полиморфизм и фабричные функции.
-
Заменяйте проверки if на ошибки.
"Guards" — это отличная концепция! Часто можно вынести вложенную проверку if
и переместить её в начало функции, чтобы быстро завершить выполнение, если какое-то условие не выполняется, и продолжить выполнение остального кода только в противном случае.
Вот пример без "guards":
function messageUser(user, message) {
if (user) {
if (message) {
if (user.acceptsMessages) {
const success = user.sendMessage(message);
if (success) {
console.log('Message sent!');
}
}
}
}
}
А вот улучшенная версия с использованием "защитного условия" и быстрого выхода:
function messageUser(user, message) {
if (!user || !message || !user.acceptsMessages) {
return;
}
const success = user.sendMessage(message);
if (success) {
console.log('Message sent!');
}
}
Благодаря выносу и объединению условий, три проверки if были объединены в одну, что позволяет функции не продолжать выполнение, если одно из условий не выполняется.
Вы просто берёте вложенные проверки, инвертируете логику (например, проверяете, что пользователь не принимает сообщения) и затем возвращаетесь или выбрасываете ошибку, чтобы избежать выполнения остальной части функции.
Выносите директивы и логику в отдельные функции
Рассмотрим пример:
function connectDatabase(uri) {
if (!uri) {
throw new Error('An URI is required!');
}
const db = new Database(uri);
let success = db.connect();
if (!success) {
if (db.fallbackConnection) {
return db.fallbackConnectionDetails;
} else {
throw new Error('Could not connect!');
}
}
return db.connectionDetails;
}
Этот код можно улучшить, применив то, что мы узнали о функциях:
function connectDatabase(uri) {
validateUri(uri);
const db = new Database(uri);
let success = db.connect();
let connectionDetails;
if (success) {
connectionDetails = db.connectionDetails;
} else {
connectionDetails = connectFallbackDatabase(db);
}
return connectionDetails;
}
function validateUri(uri) {
if (!uri) {
throw new Error('An URI is required!');
}
}
function connectFallbackDatabase(db) {
if (db.fallbackConnection) {
return db.fallbackConnectionDetails;
} else {
throw new Error('Could not connect!');
}
}
Возможно, этот код можно оптимизировать ещё больше, но уже видно, что вложенность была удалена благодаря выносу отдельной функции connectFallbackDatabase()
.
Полиморфизм и фабричные функции
Иногда вы сталкиваетесь с дублированием операторов if
и повторяющимися проверками только потому, что код внутри этих операторов немного отличается.
В таких случаях могут помочь полиморфизм и фабричные функции.
Прежде чем углубляться в эти концепции, рассмотрим пример:
function processTransaction(transaction) {
if (isPayment(transaction)) {
if (usesCreditCard(transaction)) {
processCreditCardPayment(transaction);
}
if (usesPayPal(transaction)) {
processPayPalPayment(transaction);
}
} else {
if (usesCreditCard(transaction)) {
processCreditCardRefund(transaction);
}
if (usesPayPal(transaction)) {
processPayPalRefund(transaction);
}
}
}
В этом примере мы повторяем проверки usesCreditCard()
и usesPayPal()
, потому что выполняем разный код в зависимости от того, идёт ли речь о платеже или возврате.
Мы можем решить эту проблему, написав фабричную функцию, которая возвращает полиморфный объект:
function getProcessors(transaction) {
let processors = {
processPayment: null,
processRefund: null,
};
if (usesCreditCard(transaction)) {
processors.processPayment = processCreditCardPayment;
processors.processRefund = processCreditCardRefund;
} else if (usesPayPal(transaction)) {
processors.processPayment = processPayPalPayment;
processors.processRefund = processPayPalRefund;
}
return processors;
}
function processTransaction(transaction) {
const processors = getProcessors(transaction);
if (isPayment(transaction)) {
processors.processPayment(transaction);
} else {
processors.processRefund(transaction);
}
}
Повторяющиеся проверки на использование кредитной карты или PayPal теперь вынесены в функцию getProcessors()
, которая выполняет эти проверки только один раз (вместо двух, как раньше).
Функция getProcessors()
— это фабричная функция. Она создаёт объекты, и это определение фабричной функции: функция, которая создаёт объекты.
Функция getProcessors()
возвращает объект с двумя функциями внутри — функциями, которые ещё не были выполнены (обратите внимание, что после processCreditCardPayment
и других функций отсутствуют скобки ()).
Объект, возвращаемый функцией getProcessors()
, является полиморфным, потому что мы всегда используем его одинаково (мы можем вызывать processPayment()
и processRefund()
), но логика, которая будет выполнена, не всегда одинакова.
Используйте ошибки
Ошибки — это ещё один отличный способ избавиться от избыточных проверок if
. Они позволяют использовать механизмы, встроенные в язык программирования, для обработки проблем в том месте, где они должны быть обработаны (и вызывать их там, где они должны быть вызваны).
Рассмотрим пример:
function createUser(email, password) {
const inputValidity = validateInput(email, password);
if (inputValidity.code === 1 || inputValidity.code === 2) {
console.log(inputValidity.message);
return;
}
// ... продолжение
}
function validateInput(email, password) {
if (!email.includes('@') || password.length < 7) {
return { code: 1, message: 'Invalid input' };
}
const existingUser = findUserByEmail(email);
if (existingUser) {
return { code: 2, message: 'Email is already in use!' };
}
}
Здесь функция validateInput()
не просто возвращает true
или false
. Вместо этого она возвращает объект/карту с дополнительной информацией о результате проверки. Это не нереалистичный сценарий, но он реализован неоптимально.
В конечном итоге, пример кода создаёт "синтетическую ошибку". Но поскольку она синтетическая, мы не можем обработать её с помощью стандартных инструментов обработки ошибок, вместо этого используются проверки if
.
Вот улучшенная версия, использующая встроенную поддержку ошибок, которую предлагают практически все языки программирования:
function createUser(email, password) {
try {
validateInput(email, password);
} catch (error) {
console.log(error.message);
}
// ... continue
}
function validateInput(email, password) {
if (!email.includes('@') || password.length < 7) {
throw new Error('Input is invalid!');
}
const existingUser = findUserByEmail(email);
if (existingUser) {
throw new Error('Email is already taken!');
}
}
throw
— это ключевое слово в JavaScript (и многих других языках), которое используется для генерации ошибки.
Как только ошибка "запущена", она будет всплывать через весь стек вызовов и прерывать выполнение любых функций, пока не будет обработана (с помощью try-catch
).
Это устраняет необходимость в дополнительных проверках if
и операторах return
.
Более того, мы могли бы даже вынести всю логику обработки ошибок из функции createUser()
.
function handleSignupRequest(request) {
try {
createUser(request.email, request.password)
} catch (error) {
console.log(error.message);
}
}
function createUser(email, password) {
validateInput(email, password);
// ... continue
}
function validateInput(email, password) {
if (!email.includes('@') || password.length < 7) {
throw new Error('Input is invalid!');
}
const existingUser = findUserByEmail(email);
if (existingUser) {
throw new Error('Email is already taken!');
}
}
Действительно, обработка ошибок обычно должна считаться «одной задачей» (помните: функции должны выполнять одну задачу), поэтому её вынесение в отдельную функцию — это хорошая идея.
Автор: libererd