DevGang
Авторизоваться

Ред флаги бэкэнда — что не следует делать

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

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

1. Никогда не пишите бизнес-логику в контроллере

Контроллеры существуют только для обработки запросов и ответов. Business Logic должен быть размещен либо в отдельном contextService.file, либо в модуле, который находится отдельно от контроллера. Это является обязательным условием для вас, чтобы согласиться с этим, несмотря ни на что. Могут быть некоторые случаи, которые, по вашему мнению, требуют таких действий; в большинстве случаев их можно избежать.

Давайте рассмотрим это на примере, чтобы прояснить ситуацию:

Плохой пример

import { Request, Response } from 'express';
import bcrypt from 'bcrypt';
import { UserModel } from './models/User';

export class UserController {
  public async createUser(req: Request, res: Response): Promise<Response> {
    try {
      const { username, password } = req.body;

      // business logic put inside the controller
      const hashedPassword = await bcrypt.hash(password, 10);
      const newUser = await UserModel.create({ username, password: hashedPassword });

      return res.status(201).json(newUser);
    } catch (error) {
      return res.status(500).json({ message: 'Error creating user' });
    }
  }
}

Почему это плохо?

  • Контроллер слишком раздут, поскольку он смешивает обработку запросов с бизнес-логикой.
  • Повторное использование логики хеширования или создания пользователя в другом месте становится затруднительным.
  • Тестирование контроллера усложняется, поскольку в тестах необходимо имитировать бизнес-логику.

Хороший пример

// UserService.ts
import bcrypt from 'bcrypt';
import { UserModel } from './models/User';

export class UserService {
  public async createUser(username: string, password: string): Promise<any> {
    // business logic is encapsulated here
    const hashedPassword = await bcrypt.hash(password, 10);
    return UserModel.create({ username, password: hashedPassword });
  }
}
// UserController.ts
import { Request, Response } from 'express';
import { UserService } from './services/UserService';

export class UserController {
  private userService = new UserService();

  public async createUser(req: Request, res: Response): Promise<Response> {
    try {
      const { username, password } = req.body;

      // delegate logic to the service
      const newUser = await this.userService.createUser(username, password);

      return res.status(201).json(newUser);
    } catch (error) {
      return res.status(500).json({ message: 'Error creating user' });
    }
  }
}

Почему это хорошо?

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

Контроллер фокусируется только на обработке запросов и ответов. Бизнес-логика изолирована в выделенном классе обслуживания, что упрощает управление.

  • Возможность повторного использования

Логику сервиса теперь можно повторно использовать в других частях вашего приложения (например, в другом контроллере, фоновом задании).

  • Тестируемость

Теперь вы можете писать отдельные модульные тесты для UserService.ts и UserController. Мокинг и тестирование стали намного проще.

2. Пренебрежение DTO для раскрытия данных (утечка конфиденциальных данных)

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

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

Давайте рассмотрим пример, чтобы сделать это предельно ясным:

Плохой пример

import { Request, Response } from 'express';
import { UserModel } from './models/User';

export class UserController {
  public async getUser(req: Request, res: Response): Promise<Response> {
    try {
      const userId = req.params.id;

      // fetch user directly from the database also business logic inside the controller
      const user = await UserModel.findById(userId);

      if (!user) {
        return res.status(404).json({ message: 'User not found' });
      }

      // Returning raw data (contains sensitive fields like password)
      return res.json(user);
    } catch (error) {
      return res.status(500).json({ message: 'Error fetching user' });
    }
  }
}

Почему это плохо?

  • Раскрытие конфиденциальных данных

Необработанный объект пользователя может включать конфиденциальные поля, такие как password, isAdmin или createdAt, которые не должны быть видны на внешнем интерфейсе.

  • Отсутствие контроля

Нет контроля над тем, какие поля будут раскрыты в ответе. Отправляется весь объект, что может привести к проблемам безопасности или утечке ненужных данных.

Хороший пример

// UserDTO.ts
export class UserDTO {
  constructor(public id: string, public username: string, public email: string) {}

  // static method to map the user entity to a DTO
  public static fromEntity(user: any): UserDTO {
    return new UserDTO(user._id, user.username, user.email);
  }
}
// UserController.ts
import { Request, Response } from 'express';
import { UserModel } from './models/User';
import { UserDTO } from './dtos/UserDTO';

export class UserController {
  public async getUser(req: Request, res: Response): Promise<Response> {
    try {
      const userId = req.params.id;

      // fetch user directly from the database
      const user = await UserModel.findById(userId);

      if (!user) {
        return res.status(404).json({ message: 'User not found' });
      }

      // map user entity to DTO
      const userDTO = UserDTO.fromEntity(user);

      // return the controlled user DTO, not raw data
      return res.json(userDTO);
    } catch (error) {
      return res.status(500).json({ message: 'Error fetching user' });
    }
  }
}

Почему это хорошо?

  • В DTO включены только необходимые поля, что предотвращает раскрытие конфиденциальных данных.

3. Никогда не используйте синхронный код в асинхронных операциях

Это просто: никогда не смешивайте синхронный код с асинхронными операциями. Это как пытаться пробежать марафон со сломанной ногой — это замедлит вас.

Когда вы пишете асинхронный код, вы говорите системе: «Эй, продолжай и выполняй эту задачу, пока я делаю другие дела». Если вы внезапно добавляете синхронный код в микс, вы блокируете весь процесс. Это как пробка на шоссе — все остальное должно ждать, пока эта медленно движущаяся машина уйдет с дороги.

Когда вы используете синхронные операции (например, fs.readFileSync или JSON.parse()), вы заставляете систему останавливаться, пока эта операция завершается, не давая другим задачам обрабатываться. Это становится особенно опасным в высококонкурентной среде, такой как веб-серверы, где задержка может напрямую влиять на пользовательский опыт.

Никогда не делайте этого, если вы хотите, чтобы ваша система масштабировалась и быстро реагировала. Придерживайтесь асинхронных методов, и ваше приложение будет работать гладко, быстро и эффективно.

Давайте рассмотрим это на примере кода:

Плохой пример

// UserProfileService.ts
import fs from 'fs';
import { UserProfile } from '../models/UserProfile';

export class UserProfileService {
  public async getUserProfile(userId: string): Promise<UserProfile> {
    try {
      // synchronous operation: this blocks the event loop until the file is fully read
      const data = fs.readFileSync(`./data/users/${userId}.json`, 'utf-8');

      // parse JSON data and return the user profile
      const userProfile: UserProfile = JSON.parse(data);
      return userProfile;
    } catch (error) {
      throw new Error('User profile not found');
    }
  }
}

Почему это плохо?

  • Блокировка цикла событий

fs.readFileSync() является синхронным, поэтому когда сервер читает файл, он блокирует цикл событий. При чтении профиля одного пользователя другие входящие запросы должны ждать. Это является основным узким местом производительности.

  • Проблемы масштабируемости

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

Хороший пример

// UserProfileService.ts
import fs from 'fs/promises';
import { UserProfile } from '../models/UserProfile';

export class UserProfileService {
  public async getUserProfile(userId: string): Promise<UserProfile> {
    try {
      // asynchronous operation: non-blocking, doesn't hold up the event loop
      const data = await fs.readFile(`./data/users/${userId}.json`, 'utf-8');

      // parse JSON data and return the user profile
      const userProfile: UserProfile = JSON.parse(data);
      return userProfile;
    } catch (error) {
      throw new Error('User profile not found');
    }
  }
}

Почему это хорошо?

  • Неблокирующий

Использование fs.readFile() из fs/promises модуля делает операцию чтения файла асинхронной. Await гарантирует, что во время ожидания чтения файла другие задачи (например, ответы на различные HTTP-запросы) будут обрабатываться без какой-либо задержки.

  • Масштабируемый

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

  • Лучший пользовательский опыт

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

4. Слишком общая обработка ошибок — кошмар отладки

Давайте поговорим об одной из самых неприятных ошибок, которые вы можете совершить при работе с ошибками в вашем приложении: слишком общая обработка ошибок. Вероятно, вы уже сталкивались с этим — ошибки слишком неопределенные или слишком повторяющиеся, чтобы быть полезными. Когда вы перехватываете ошибки и просто выдаете или регистрируете общее сообщение, например «Что-то пошло не так» или «Произошла ошибка», вы по сути усложняете отладку для себя и своей команды. Контекст — это все, и общее сообщение об ошибке оставит вас только в догадках, что на самом деле пошло не так. Отладка должна заключаться в отслеживании первопричины, а не в том, чтобы снова и снова смотреть на одно и то же бессмысленное сообщение.

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

Плохой пример

// PaymentService.ts
import axios from 'axios';

export class PaymentService {
  private paymentAPI = 'https://paymentgateway.com/api/payment';

  // method to process payment
  public async processPayment(userId: string, amount: number): Promise<boolean> {
    try {
      const response = await axios.post(this.paymentAPI, { userId, amount });

      if (response.data.success) {
        return true;
      } else {
        throw new Error('Payment failed');
      }
    } catch (error) {
      // catch all errors and print a generic message
      console.error('An error occurred during payment processing');
      return false;
    }
  }
}

Почему это плохо?

  • Контекст ошибки отсутствует

Обработка ошибок является общей и не предоставляет никакой полезной информации о природе сбоя. Она не учитывает различные типы ошибок, которые могут возникнуть (например, сетевые ошибки, ошибки, специфичные для API, или ошибки, связанные с пользователем).

  • Повторяющееся сообщение

Одно и то же сообщение («Произошла ошибка при обработке платежа») регистрируется независимо от фактической проблемы. Это приводит к повторяющимся и неинформативным журналам ошибок, которые значительно затрудняют отладку.

Хороший пример

// PaymentService.ts
import axios from 'axios';

export class PaymentService {
  private paymentAPI = 'https://paymentgateway.com/api/payment';

  // method to process payment
  public async processPayment(userId: string, amount: number): Promise<boolean> {
    try {
      const response = await axios.post(this.paymentAPI, { userId, amount });

      if (response.data.success) {
        return true;
      } else {
        // handle API-specific failure scenarios
        if (response.data.errorCode === 'INSUFFICIENT_FUNDS') {
          throw new Error('Payment failed due to insufficient funds');
        } else if (response.data.errorCode === 'INVALID_CARD') {
          throw new Error('Payment failed due to invalid card');
        } else {
          throw new Error('Payment failed due to an unknown error');
        }
      }
    } catch (error: any) {
      // check if it's a network or server error
      if (error.response) {
        console.error(`Payment failed with status ${error.response.status}: ${error.response.data.message}`);
      } else if (error.request) {
        console.error('Payment request failed: No response from payment gateway');
      } else {
        console.error(`Payment failed: ${error.message}`);
      }
      return false;
    }
  }
}

Почему это хорошо?

  • Специфическая обработка ошибок

Код проверяет наличие определенных кодов ошибок, таких как INSUFFICIENT_FUNDS или INVALID_CARD, чтобы предоставить четкие, целевые сообщения.

  • Сообщения об ошибках, требующие действий

Система выдает понятные и полезные сообщения, которые помогают пользователям и разработчикам понять и устранить проблему.

  • Категоризация ошибок

Код различает разные типы ошибок (например, ошибки сети, API) для их соответствующей обработки.

  • Прозрачная отладка

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

Заключение

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

Источник:

Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу