Программирование — это и искусство, и наука, и, как и любое другое ремесло, оно включает в себя овладение набором методов и практик, которые помогут вам создавать более качественное, надежное и удобное в сопровождении программное обеспечение. В этой статье мы рассмотрим десять лучших практик, которые каждый разработчик должен знать и применять в своей повседневной работе. От соглашений об именах и организации кода до тестирования и внедрения зависимостей — эти методы не только необходимы, но и могут упростить чтение, отладку и изменение кода. Итак, являетесь ли вы опытным профессионалом или только начинаете, читайте дальше и узнайте, как вы можете повысить уровень своих навыков программирования!

№1: Но как его назвать?!

Первая лучшая практика для написания чистого кода — следовать согласованному соглашению об именах. Соглашения об именах должны быть осмысленными, описательными и согласованными во всей кодовой базе. Это облегчает разработчикам понимание того, что делает код и как он сочетается друг с другом.

Практические примеры следования согласованному соглашению об именах включают:

  1. Используйте описательные имена для переменных, функций и классов: например, вместо того, чтобы называть переменную «x», назовите ее более описательно, например «numberOfItems». Это проясняет, что представляет переменная, и делает код более читабельным.
  2. Последовательно используйте camelCase или snake_case: выберите соглашение об именах переменных и придерживайтесь его во всей кодовой базе. Например, если вы решили использовать camelCase, используйте его последовательно для всех имен переменных.
  3. Соблюдайте аббревиатуры: если вы используете аббревиатуру для имени переменной или функции, убедитесь, что она непротиворечива во всей кодовой базе. Например, если вы сокращаете «число» до «число» в одном имени переменной, то последовательно используйте «число» для всех имен переменных, в которых используется это сокращение.
  4. Используйте четкие и осмысленные имена функций: функции должны иметь имена, точно описывающие, что они делают. Например, функция, вычисляющая общую стоимость товаров в корзине, должна называться как-то вроде «calculateTotalPrice», а не «function1».
  5. Используйте согласованное форматирование для соглашений об именах: согласованность является ключевым моментом, когда речь идет о соглашениях об именах. Выберите стиль форматирования для своей кодовой базы и придерживайтесь его во всем. Например, если вы решите сделать первую букву каждого слова в имени переменной заглавной, сделайте это последовательно для всех имен переменных.
  6. Избегайте однобуквенных имен переменных: Использование однобуквенных имен переменных, таких как «i» или «j», может затруднить понимание другими разработчиками назначения переменной. Вместо этого используйте более описательные имена, обеспечивающие контекст и ясность.
  7. Учитывайте контекст кода: соглашение об именах должно соответствовать контексту кода. Например, имя переменной в математической функции должно быть названо в соответствии с ее использованием в математике. И наоборот, имя переменной в функции, вычисляющей цену продукта, может быть названо в зависимости от контекста торговли.

Пример 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
}

Вот еще несколько примеров того, как избежать магических чисел или строк в вашем коде:

  1. Используйте константы для часто используемых значений:
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: Напишите автоматические модульные тесты, чтобы убедиться в правильности и надежности кода

Модульное тестирование является важным аспектом разработки программного обеспечения, который помогает обеспечить правильность и надежность кода. Автоматизированные модульные тесты могут обнаруживать ошибки на ранних этапах цикла разработки и предотвращать регрессию по мере развития кодовой базы.

Чтобы написать эффективные модульные тесты, вы должны следовать следующим рекомендациям:

  1. Напишите тесты для каждой единицы кода: каждая функция, метод или класс должны иметь один или несколько тестов, которые охватывают все возможные сценарии и пограничные случаи.
  2. Используйте среду тестирования: среда тестирования предоставляет набор инструментов и соглашений, которые помогут вам писать, организовывать и запускать тесты. Популярные среды тестирования для JavaScript включают Jest, Mocha и Jasmine.
  3. Используйте внедрение зависимостей и инверсию управления: при написании модульных тестов важно изолировать тестируемый код от его зависимостей. Вы можете добиться этого, используя внедрение зависимостей и инверсию управления для внедрения фиктивных или заглушенных зависимостей.
  4. Используйте шпионов для проверки поведения: шпионы — это тестовые двойники, которые позволяют отслеживать и проверять поведение функции или метода. Вы можете использовать их, чтобы убедиться, что тестируемый код вызывает ожидаемые методы с ожидаемыми аргументами.

Вот пример того, как использовать модульное тестирование, внедрение зависимостей, инверсию управления и шпионов в приложении 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:

  1. Интегрируйте SonarQube в конвейер CI/CD: убедитесь, что SonarQube является частью вашего автоматизированного процесса сборки. Это гарантирует, что ваш код анализируется на наличие проблем с качеством при каждой сборке.
  2. Установите пороги качества: пороги качества — это пороговые значения, которым должен соответствовать ваш код, прежде чем его можно будет рассмотреть для выпуска. Установите пороги качества для своих проектов и убедитесь, что все изменения кода соответствуют этим пороговым значениям, прежде чем они будут развернуты.
  3. Быстро устраняйте проблемы: когда SonarQube отмечает проблему, решайте ее как можно быстрее. Чем дольше вы откладываете решение проблемы, тем больше вероятность того, что она станет более серьезной проблемой.
  4. Настройте правила: SonarQube поставляется с набором предопределенных правил, но вы также можете настроить их в соответствии со своими конкретными потребностями. Потратьте время на просмотр правил по умолчанию и добавьте любые дополнительные правила, важные для вашей организации.
  5. Проанализируйте результаты: используйте информационные панели и отчеты SonarQube для анализа результатов анализа кода. Это поможет вам определить области, где вам нужно улучшить и внести изменения в процесс разработки.
  6. Вовлекайте свою команду: SonarQube — это инструмент для всей команды, а не только для разработчиков. Убедитесь, что все в команде понимают, как им пользоваться и почему это важно.
  7. Следите за своим техническим долгом: Технический долг — это стоимость поддержки вашего кода в течение долгого времени. SonarQube может помочь вам определить области вашей кодовой базы, обслуживание которых обходится дороже, и позволит вам расставить приоритеты для рефакторинга.

Следуя этим передовым методам, вы можете убедиться, что SonarQube является эффективным инструментом для постоянного улучшения качества вашего кода.

В заключение, следование передовым методам необходимо для создания высококачественных, удобных в сопровождении и масштабируемых программных систем. Следуя этим рекомендациям, разработчики могут снизить риск появления ошибок, упростить понимание и сопровождение кода, повысить производительность и повысить общее качество кодовой базы. Более того, соблюдение лучших практик может улучшить совместную работу и общение между членами команды и может гарантировать согласованность кода и его соответствие отраслевым стандартам.

Хотите узнать больше?

Если вы хотите узнать больше о чистом коде и ремонтопригодности, ознакомьтесь с некоторыми из следующих материалов:

  1. Чистый код: руководство по Agile Software Craftsmanship (книга) Роберта К. Мартина
  2. Чистый код JavaScript (книга) Райана Макдермотта
  3. Чистая архитектура: руководство мастера по структуре и дизайну программного обеспечения (книга) Роберта С. Мартина
  4. Блог чистого кода (блог) Роберта К. Мартина
  5. Разговоры о чистом коде (видео) Роберта С. Мартина

Если вам понравился этот контент и он оказался полезным, пожалуйста, хлопните в ладоши 👏 и подпишитесь, чтобы выразить свою поддержку 😻. Кроме того, если у вас есть дополнительные вопросы или вы хотите, чтобы я подготовил больше контента по определенной теме, не стесняйтесь спрашивать. Спасибо за чтение!