Программирование — это и искусство, и наука, и, как и любое другое ремесло, оно включает в себя овладение набором методов и практик, которые помогут вам создавать более качественное, надежное и удобное в сопровождении программное обеспечение. В этой статье мы рассмотрим десять лучших практик, которые каждый разработчик должен знать и применять в своей повседневной работе. От соглашений об именах и организации кода до тестирования и внедрения зависимостей — эти методы не только необходимы, но и могут упростить чтение, отладку и изменение кода. Итак, являетесь ли вы опытным профессионалом или только начинаете, читайте дальше и узнайте, как вы можете повысить уровень своих навыков программирования!
№1: Но как его назвать?!
Первая лучшая практика для написания чистого кода — следовать согласованному соглашению об именах. Соглашения об именах должны быть осмысленными, описательными и согласованными во всей кодовой базе. Это облегчает разработчикам понимание того, что делает код и как он сочетается друг с другом.
Практические примеры следования согласованному соглашению об именах включают:
- Используйте описательные имена для переменных, функций и классов: например, вместо того, чтобы называть переменную «x», назовите ее более описательно, например «numberOfItems». Это проясняет, что представляет переменная, и делает код более читабельным.
- Последовательно используйте camelCase или snake_case: выберите соглашение об именах переменных и придерживайтесь его во всей кодовой базе. Например, если вы решили использовать camelCase, используйте его последовательно для всех имен переменных.
- Соблюдайте аббревиатуры: если вы используете аббревиатуру для имени переменной или функции, убедитесь, что она непротиворечива во всей кодовой базе. Например, если вы сокращаете «число» до «число» в одном имени переменной, то последовательно используйте «число» для всех имен переменных, в которых используется это сокращение.
- Используйте четкие и осмысленные имена функций: функции должны иметь имена, точно описывающие, что они делают. Например, функция, вычисляющая общую стоимость товаров в корзине, должна называться как-то вроде «calculateTotalPrice», а не «function1».
- Используйте согласованное форматирование для соглашений об именах: согласованность является ключевым моментом, когда речь идет о соглашениях об именах. Выберите стиль форматирования для своей кодовой базы и придерживайтесь его во всем. Например, если вы решите сделать первую букву каждого слова в имени переменной заглавной, сделайте это последовательно для всех имен переменных.
- Избегайте однобуквенных имен переменных: Использование однобуквенных имен переменных, таких как «i» или «j», может затруднить понимание другими разработчиками назначения переменной. Вместо этого используйте более описательные имена, обеспечивающие контекст и ясность.
- Учитывайте контекст кода: соглашение об именах должно соответствовать контексту кода. Например, имя переменной в математической функции должно быть названо в соответствии с ее использованием в математике. И наоборот, имя переменной в функции, вычисляющей цену продукта, может быть названо в зависимости от контекста торговли.
Пример 1 — Несогласованное соглашение об именах:
function calculate(a, b) { let c = a + b; return c; }
В этом примере соглашение об именах непоследовательно. Имя функции является описательным, а имена переменных — нет. a
и b
не очень информативны, а c
можно назвать как-то более осмысленно, например sum
. Это затрудняет чтение и понимание кода.
Пример 2 — Соглашение об описательных именах:
function calculateSum(firstNumber, secondNumber) { let sum = firstNumber + secondNumber; return sum; }
В этом примере соглашение об именах является последовательным и описательным. Имя функции точно описывает, что делает функция, а имена переменных обеспечивают четкий контекст и значение. Это облегчает чтение и понимание кода.
Пример 3 — постоянное использование сокращений:
function calculateTax(subtotal, taxRate) { let tax = subtotal * taxRate; return tax; }
В этом примере аббревиатура «налог» последовательно используется во всей функции, чтобы было понятно, что представляет собой переменная. Это улучшает читаемость кода и облегчает другим разработчикам понимание того, что делает функция.
#2: Функции должны быть маленькими и даже меньшими!
Вторая лучшая практика для написания чистого кода — это написание небольших и целенаправленных функций. Функции должны иметь четкую и конкретную цель и должны делать одну вещь, и делать это хорошо. Это упрощает чтение, тестирование и сопровождение кода.
Практические примеры написания небольших и целенаправленных функций включают в себя:
Разбиение сложных функций на более мелкие. Если функция слишком длинная или сложная, ее может быть сложно понять и поддерживать. Разбив его на более мелкие функции, каждая из которых имеет определенную цель, код становится легче понять и изменить.
// Complex function function calculateTotalPrice(items) { let totalPrice = 0; for (let i = 0; i < items.length; i++) { totalPrice += items[i].price; } let tax = totalPrice * 0.1; let finalPrice = totalPrice + tax; return finalPrice; } // Refactored function function calculateTotalPrice(items) { let totalPrice = calculateSubtotal(items); let tax = calculateTax(totalPrice); let finalPrice = totalPrice + tax; return finalPrice; } function calculateSubtotal(items) { let totalPrice = 0; for (let i = 0; i < items.length; i++) { totalPrice += items[i].price; } return totalPrice; } function calculateTax(subtotal) { return subtotal * 0.1; }
Избегайте «побочных эффектов» в функциях: функция должна делать только то, для чего она предназначена, и не должна иметь каких-либо непреднамеренных побочных эффектов в других частях кода. Это упрощает тестирование и отладку кода.
// Function with side effects let totalPrice = 0; function calculateTotalPrice(items) { for (let i = 0; i < items.length; i++) { totalPrice += items[i].price; } } // Refactored function without side effects function calculateTotalPrice(items) { let totalPrice = 0; for (let i = 0; i < items.length; i++) { totalPrice += items[i].price; } return totalPrice; }
Сохранение функций «чистыми» и независимыми. Чистая функция — это функция, не имеющая побочных эффектов и всегда возвращающая одни и те же выходные данные для одних и тех же входных данных. Это упрощает анализ кода и его тестирование.
// Impure function let taxRate = 0.1; function calculateTax(subtotal) { return subtotal * taxRate; } // Refactored pure function function calculateTax(subtotal, taxRate) { return subtotal * taxRate; }
№3: Волшебство, волшебство повсюду!
Третий лучший способ написания чистого кода — избегать использования магических чисел или магических строк. Магическое число или строка — это значение, которое появляется в коде без каких-либо объяснений или контекста, что затрудняет его понимание и поддержку.
Например, представьте, что вы работаете над кодовой базой со следующей строкой кода:
if (statusCode === 404) { // Do something }
В данном случае 404
— магическое число. Непонятно, почему используется этот номер, и он может ввести в заблуждение тех, кто не знаком с кодом.
Чтобы избежать этой проблемы, вы можете определить константу или переменную для представления магического числа:
const NOT_FOUND = 404; if (statusCode === NOT_FOUND) { // Do something }
Вот еще несколько примеров того, как избежать магических чисел или строк в вашем коде:
- Используйте константы для часто используемых значений:
const PI = 3.14159; const MAX_ATTEMPTS = 5; let circleArea = PI * radius * radius; for (let i = 0; i < MAX_ATTEMPTS; i++) { // Try something up to 5 times }
2. Используйте перечисления или объекты для дискретных значений:
const Color = { RED: 'red', BLUE: 'blue', GREEN: 'green' }; let carColor = Color.RED; if (carColor === Color.BLUE) { // Do something }
3. Используйте описательные имена переменных:
const SECONDS_IN_AN_HOUR = 3600; let timeElapsedInSeconds = 5400; // 1.5 hours let timeElapsedInHours = timeElapsedInSeconds / SECONDS_IN_AN_HOUR;
И помните, хотя фокусы могут быть впечатляющими на сцене, магическим числам и строкам не место в вашем коде!
#4: Сделайте функции еще меньше! — Одна функция, одна ответственность
Как я уже писал ранее, при написании кода важно, чтобы функции были небольшими и сфокусированными. У каждой функции должна быть одна четкая обязанность, что упрощает ее понимание, тестирование и поддержку.
С большими монолитными функциями может быть сложно работать, и это может затруднить выявление и исправление ошибок. Разбивая функции на более мелкие, целенаправленные части, вы можете создавать более модульный код, с которым легче работать.
Вот пример большой монолитной функции:
function validateUser(user) { if (!user) { return false; } if (!user.username || !user.password) { return false; } if (user.username.length < 6 || user.password.length < 8) { return false; } // Check for duplicate username let users = getUsers(); for (let i = 0; i < users.length; i++) { if (users[i].username === user.username) { return false; } } // Check for strong password let passwordRegex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}/; if (!passwordRegex.test(user.password)) { return false; } // Validation successful return true; }
Эта функция делает слишком много вещей одновременно — проверяет объект пользователя, проверяет наличие повторяющихся имен пользователей и проверяет надежный пароль. Вместо этого мы можем разбить эту функцию на более мелкие, целенаправленные части:
function validateUser(user) { if (!user) { return false; } if (!isValidUsername(user.username)) { return false; } if (!isValidPassword(user.password)) { return false; } if (isDuplicateUsername(user.username)) { return false; } // Validation successful return true; } function isValidUsername(username) { return !!username && username.length >= 6; } function isValidPassword(password) { let passwordRegex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}/; return !!password && passwordRegex.test(password); } function isDuplicateUsername(username) { let users = getUsers(); for (let i = 0; i < users.length; i++) { if (users[i].username === username) { return true; } } return false; }
Теперь у каждой функции есть одна четкая обязанность, а код легче читать, понимать и поддерживать.
# 5: Не повторяйтесь (СУХОЙ) — потому что копипастинг — для любителей
При написании кода важно следовать принципу СУХОЙ — Не повторяйтесь. Это означает, что вы должны максимально избегать дублирования кода и вместо этого находить способы повторного использования уже написанного кода.
Копирование и вставка кода может показаться быстрым и простым решением, но в долгосрочной перспективе это часто приводит к увеличению объема работы. Дублированный код сложнее поддерживать и обновлять, он может привести к несоответствиям и ошибкам.
Поэтому вместо копирования-вставки попробуйте найти способы повторного использования кода. Это может означать создание функций, классов или модулей, которые можно повторно использовать в вашей кодовой базе.
Вот пример кода, нарушающего принцип DRY:
function calculatePrice(quantity, price) { let taxRate = 0.10; let subtotal = quantity * price; let tax = subtotal * taxRate; let total = subtotal + tax; return total; } function calculateCost(quantity, price) { let taxRate = 0.10; let subtotal = quantity * price; let tax = subtotal * taxRate; let total = subtotal + tax; let shippingFee = 10; let cost = total + shippingFee; return cost; }
Как видите, функции calculatePrice
и calculateCost
практически идентичны — обе они вычисляют промежуточный итог, налог и итог. Вместо того, чтобы дублировать этот код, мы можем выделить его в отдельную функцию:
function calculatePrice(quantity, price) { let subtotal = quantity * price; let tax = calculateTax(subtotal); let total = subtotal + tax; return total; } function calculateCost(quantity, price) { let subtotal = quantity * price; let tax = calculateTax(subtotal); let total = subtotal + tax; let shippingFee = 10; let cost = total + shippingFee; return cost; } function calculateTax(subtotal) { let taxRate = 0.10; return subtotal * taxRate; }
Теперь код СУХОЙ — мы повторно используем функцию calculateTax
, чтобы избежать дублирования кода. И, надеюсь, вы посмеялись над идеей, что копипастинг — удел любителей!
# 7: Предпочитайте композицию наследованию
В объектно-ориентированном программировании наследование — это способ создания нового класса на основе существующего класса, наследующего все его свойства и методы. Однако чрезмерное использование наследования может привести к сильно связанному дизайну, что затруднит сопровождение и модификацию кода.
Лучшим подходом является использование композиции, когда класс состоит из других объектов, каждый из которых отвечает за определенное поведение. Это обеспечивает более гибкий дизайн, в котором поведение можно добавлять или удалять, не затрагивая другие части кода.
Вот пример использования композиции вместо наследования:
class Animal { constructor(name) { this.name = name; } } class CanFly { fly() { console.log(`${this.name} is flying.`); } } class CanSwim { swim() { console.log(`${this.name} is swimming.`); } } class Duck extends Animal { constructor(name) { super(name); this.flyBehavior = new CanFly(); this.swimBehavior = new CanSwim(); } fly() { this.flyBehavior.fly(); } swim() { this.swimBehavior.swim(); } }
В этом примере у нас есть три класса: Animal
, CanFly
и CanSwim
. Animal
— это базовый класс, предоставляющий общий атрибут name
для всех животных. CanFly
и CanSwim
— это классы, обеспечивающие поведение для полета и плавания соответственно.
Класс Duck
использует композицию для включения поведения полета и плавания. У него есть два атрибута, fly_behavior
и swim_behavior
, которые являются экземплярами классов CanFly
и CanSwim
соответственно. Затем класс Duck
определяет свои собственные методы fly
и swim
, которые делегируют соответствующие методы в объектах поведения.
Использование композиции таким образом позволяет нам создавать более гибкий и модульный код, в котором поведение можно легко добавлять или удалять, не затрагивая другие части кода.
# 8: Используйте внедрение зависимостей и инверсию управления для управления зависимостями
Управление зависимостями является критическим аспектом проектирования программного обеспечения, поскольку оно определяет, как различные компоненты системы взаимодействуют друг с другом. Одним из распространенных подходов к управлению зависимостями является внедрение зависимостей и инверсия управления.
Внедрение зависимостей (DI) — это шаблон проектирования, который позволяет компонентам быть слабо связанными путем внедрения их зависимостей, а не их внутреннего создания. Инверсия управления (IoC) — это связанный шаблон, который позволяет отделить компоненты от их конкретных реализаций путем делегирования контроля над созданием объектов и управлением ими внешнему контейнеру или фреймворку.
Используя DI и IoC, вы можете создать более модульную и тестируемую кодовую базу, а также отделить компоненты от их конкретных реализаций, сделав их более гибкими и адаптируемыми к изменениям.
Вот пример того, как DI и IoC можно использовать в приложении JavaScript:
class UserService { constructor(userRepository) { this.userRepository = userRepository; } getUser(userId) { return this.userRepository.getUserById(userId); } } class UserRepository { constructor(database) { this.database = database; } getUserById(userId) { return this.database.query(`SELECT * FROM users WHERE id = ${userId}`); } } class Database { constructor(connectionString) { this.connectionString = connectionString; } query(queryString) { // Execute the query using the database connection } } // Create a new database instance with the connection string const database = new Database('postgres://user:password@localhost/mydatabase'); // Create a new user repository instance with the database instance const userRepository = new UserRepository(database); // Create a new user service instance with the user repository instance const userService = new UserService(userRepository); // Use the user service to get a user by ID const user = userService.getUser(123);
В этом примере у нас есть три класса: UserService
, UserRepository
и Database
. Класс UserService
зависит от класса UserRepository
, который, в свою очередь, зависит от класса Database
.
Вместо внутреннего создания зависимостей мы используем DI для внедрения их в качестве аргументов конструктора. Мы также используем IoC для делегирования создания объектов и управления ими внешнему контейнеру, в данном случае вручную создавая экземпляры объектов и передавая их друг другу.
Использование DI и IoC таким образом помогает нам создать более модульную и тестируемую кодовую базу, а также отделить компоненты от их конкретных реализаций, сделав их более гибкими и адаптируемыми к изменениям.
# 9: Напишите автоматические модульные тесты, чтобы убедиться в правильности и надежности кода
Модульное тестирование является важным аспектом разработки программного обеспечения, который помогает обеспечить правильность и надежность кода. Автоматизированные модульные тесты могут обнаруживать ошибки на ранних этапах цикла разработки и предотвращать регрессию по мере развития кодовой базы.
Чтобы написать эффективные модульные тесты, вы должны следовать следующим рекомендациям:
- Напишите тесты для каждой единицы кода: каждая функция, метод или класс должны иметь один или несколько тестов, которые охватывают все возможные сценарии и пограничные случаи.
- Используйте среду тестирования: среда тестирования предоставляет набор инструментов и соглашений, которые помогут вам писать, организовывать и запускать тесты. Популярные среды тестирования для JavaScript включают Jest, Mocha и Jasmine.
- Используйте внедрение зависимостей и инверсию управления: при написании модульных тестов важно изолировать тестируемый код от его зависимостей. Вы можете добиться этого, используя внедрение зависимостей и инверсию управления для внедрения фиктивных или заглушенных зависимостей.
- Используйте шпионов для проверки поведения: шпионы — это тестовые двойники, которые позволяют отслеживать и проверять поведение функции или метода. Вы можете использовать их, чтобы убедиться, что тестируемый код вызывает ожидаемые методы с ожидаемыми аргументами.
Вот пример того, как использовать модульное тестирование, внедрение зависимостей, инверсию управления и шпионов в приложении JavaScript:
class UserService { constructor(userRepository) { this.userRepository = userRepository; } async createUser(user) { // Validate the user input if (!user.name || !user.email) { throw new Error('Invalid user input'); } // Save the user in the database const savedUser = await this.userRepository.saveUser(user); // Send a welcome email to the user await this.sendWelcomeEmail(savedUser); return savedUser; } async sendWelcomeEmail(user) { // Send a welcome email to the user } } class MockUserRepository { constructor() { this.users = new Map(); } async saveUser(user) { this.users.set(user.id, user); return user; } async getUserById(userId) { return this.users.get(userId); } } describe('UserService', () => { let userService; let mockUserRepository; beforeEach(() => { mockUserRepository = new MockUserRepository(); userService = new UserService(mockUserRepository); }); describe('createUser', () => { it('should save the user in the repository', async () => { const user = { id: 1, name: 'John Doe', email: '[email protected]' }; const savedUser = await userService.createUser(user); const retrievedUser = await mockUserRepository.getUserById(1); expect(retrievedUser).toEqual(savedUser); }); it('should throw an error if the user input is invalid', async () => { const user = { id: 1, name: 'John Doe' }; await expect(userService.createUser(user)).rejects.toThrow('Invalid user input'); }); it('should send a welcome email to the user', async () => { const user = { id: 1, name: 'John Doe', email: '[email protected]' }; // Create a spy for the sendWelcomeEmail method const spy = jest.spyOn(userService, 'sendWelcomeEmail'); await userService.createUser(user); expect(spy).toHaveBeenCalledWith(user); }); }); });
Этот пример кода демонстрирует важность модульного тестирования в разработке программного обеспечения. В этом примере у нас есть класс UserService, который зависит от класса UserRepository для сохранения и извлечения пользователей из базы данных. Мы используем внедрение зависимостей для внедрения фиктивного экземпляра UserRepository в модульные тесты.
В классе UserService есть метод createUser, который создает нового пользователя и сохраняет его в базе данных. Сначала он проверяет ввод пользователя и выдает ошибку, если он недействителен. Затем он сохраняет пользователя в базе данных, используя внедренный экземпляр UserRepository. Наконец, он отправляет приветственное письмо пользователю.
Мы также создали класс MockUserRepository, который имитирует базу данных, сохраняя пользователей на карте. Этот класс имеет два метода: saveUser и getUserById. Метод saveUser сохраняет пользователя на карте, а метод getUserById извлекает пользователя по его идентификатору.
В модульных тестах мы создаем экземпляр класса UserService и внедряем экземпляр MockUserRepository. Затем мы пишем тесты для метода createUser, которые проверяют, сохраняет ли он пользователя в репозитории, выдает ошибку, если пользовательский ввод недействителен, и отправляет приветственное письмо пользователю.
Мы также используем шпион, чтобы проверить, вызывается ли метод sendWelcomeEmail при создании нового пользователя. Шпион — это функция, которая записывает все вызовы к ней, поэтому мы можем проверить, была ли она вызвана с правильными аргументами.
В целом, этот пример демонстрирует, как мы можем использовать модульное тестирование и внедрение зависимостей, чтобы проверить наш код и убедиться, что он работает должным образом.
№10. Используйте SonarQube для непрерывной проверки качества кода
SonarQube — отличный инструмент для постоянной проверки качества вашего кода. Интегрировав его с конвейером CI/CD, вы сможете автоматически анализировать каждое новое изменение кода на наличие проблем с качеством и сразу же получать отзывы. Вот несколько советов по эффективному использованию SonarQube:
- Интегрируйте SonarQube в конвейер CI/CD: убедитесь, что SonarQube является частью вашего автоматизированного процесса сборки. Это гарантирует, что ваш код анализируется на наличие проблем с качеством при каждой сборке.
- Установите пороги качества: пороги качества — это пороговые значения, которым должен соответствовать ваш код, прежде чем его можно будет рассмотреть для выпуска. Установите пороги качества для своих проектов и убедитесь, что все изменения кода соответствуют этим пороговым значениям, прежде чем они будут развернуты.
- Быстро устраняйте проблемы: когда SonarQube отмечает проблему, решайте ее как можно быстрее. Чем дольше вы откладываете решение проблемы, тем больше вероятность того, что она станет более серьезной проблемой.
- Настройте правила: SonarQube поставляется с набором предопределенных правил, но вы также можете настроить их в соответствии со своими конкретными потребностями. Потратьте время на просмотр правил по умолчанию и добавьте любые дополнительные правила, важные для вашей организации.
- Проанализируйте результаты: используйте информационные панели и отчеты SonarQube для анализа результатов анализа кода. Это поможет вам определить области, где вам нужно улучшить и внести изменения в процесс разработки.
- Вовлекайте свою команду: SonarQube — это инструмент для всей команды, а не только для разработчиков. Убедитесь, что все в команде понимают, как им пользоваться и почему это важно.
- Следите за своим техническим долгом: Технический долг — это стоимость поддержки вашего кода в течение долгого времени. SonarQube может помочь вам определить области вашей кодовой базы, обслуживание которых обходится дороже, и позволит вам расставить приоритеты для рефакторинга.
Следуя этим передовым методам, вы можете убедиться, что SonarQube является эффективным инструментом для постоянного улучшения качества вашего кода.
В заключение, следование передовым методам необходимо для создания высококачественных, удобных в сопровождении и масштабируемых программных систем. Следуя этим рекомендациям, разработчики могут снизить риск появления ошибок, упростить понимание и сопровождение кода, повысить производительность и повысить общее качество кодовой базы. Более того, соблюдение лучших практик может улучшить совместную работу и общение между членами команды и может гарантировать согласованность кода и его соответствие отраслевым стандартам.
Хотите узнать больше?
Если вы хотите узнать больше о чистом коде и ремонтопригодности, ознакомьтесь с некоторыми из следующих материалов:
- Чистый код: руководство по Agile Software Craftsmanship (книга) Роберта К. Мартина
- Чистый код JavaScript (книга) Райана Макдермотта
- Чистая архитектура: руководство мастера по структуре и дизайну программного обеспечения (книга) Роберта С. Мартина
- Блог чистого кода (блог) Роберта К. Мартина
- Разговоры о чистом коде (видео) Роберта С. Мартина
Если вам понравился этот контент и он оказался полезным, пожалуйста, хлопните в ладоши 👏 и подпишитесь, чтобы выразить свою поддержку 😻. Кроме того, если у вас есть дополнительные вопросы или вы хотите, чтобы я подготовил больше контента по определенной теме, не стесняйтесь спрашивать. Спасибо за чтение!