Четыре пункта, как улучшить код Backend стажера

от автора

Код, разобранный в статье, можно посмотреть в этом репозитории

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

Однако, нет четкого критерия, с какого объема компоновать код в одну программу (микросервис) становится сложно. Это зависит от навыка программиста: количества лет коммерческого опыта и желания писать код начисто. Оба показателя у новых кадров, после появления IT курсов, падают: работодатели не оплачивают рост своих кадров, а рынок заполонили образовательные организации, ориентированные на малый бюджет потребителя, а не полноту знаний.

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

Инъекция зависимостей

В SOLID для инъекции зависимостей выделена отдельная буква. Это не просто так: соединять существующий код проще, когда он изначально создается как конструктор Lego: кубики стандартизируются скорее слотами соединения, а не своей формой.

config/TYPES.ts

const baseServices = {     loggerService: Symbol('loggerService'),     contextService: Symbol('contextService'), };  const dbServices = {     dataDbService: Symbol('dataDbService'), };  const viewServices = {     dataViewService: Symbol('dataViewService'), };  const TYPES = {     ...baseServices,     ...dbServices,     ...viewServices, };  export default TYPES;

config/provide.ts

import { provide } from 'di-kit'; import TYPES from './types';  {     provide(TYPES.loggerService, () => new LoggerService());     provide(TYPES.contextService, () => new ContextService()); }  {     provide(TYPES.dataDbService, () => new DataDbService()); }  {     provide(TYPES.dataViewService, () => new DataViewService()); }

index.ts

import { inject, init } from 'di-kit'; import TYPES from './config/types';  const baseServices = {     loggerService: inject(TYPES.loggerService),     contextService: inject(TYPES.contextService), };  const dbServices = {     dataDbService: inject(TYPES.dataDbService), };  const viewServices = {     dataViewService: inject(TYPES.dataViewService), };  const ioc = {     ...baseServices,     ...dbServices,     ...viewServices, };  init();

Важно соблюсти три критерия

1. Не использовать синтаксис декораторов

Декораторы изначально это экспериментальный синтаксис TypeScript, поэтому, его не получится прозрачно перенести в REPL или точно проанализировать поведение языковой моделью, так как, когда-нибудь TC39 проведут proposal и весь код будет deprecated

2. Использовать контейнер, умеющий разрешать циклические зависимости

Циклическая зависимость это ошибка, однако, иногда класс нужно разнести на две части. В C# для этого выделено отдельное ключевое слово partial. Вам точно потребуется, когда вы захотите отделить запросы авторизации/регистрации и продления от сервиса хранения и проверки JWT токена.

3. Инъекция зависимостей не должна зависеть от бандлера

Использования компилятора зависимостей не даст запустить проект в режиме интерпретации через ts-note, Bun или Deno

Scoped сервисы

В C#, для создания сервисов инстанцируемых в контексте исполнения HTTP запроса, используются Transient services

services/base/ContextService.ts

import { scoped } from "di-scoped";  export interface IContext {     serviceName: string;     clientId: string;     userId: string;     requestId: string; }  export const ContextService = scoped(     class {         constructor(readonly context: IContext) {}     } );  export type TContextService = InstanceType;  export default ContextService;

services/base/LoggerService.ts

import { log } from 'pinolog'; import { inject } from 'di-kit'; import { TContextService } from './ContextService'; import TYPES from 'src/config/types';  export class LoggerService {     protected readonly contextService = inject(TYPES.contextService);      public log = (...args: any[]) => {         log(...args);     }      public logCtx = (...args: any[]) => {         log(...args, this.contextService.context);     }; }  export default LoggerService;

services/view/DataViewService.tsx

import { log } from 'pinolog'; import { inject } from 'di-kit'; import { LoggerService } from '../base/LoggerService'; import DataDbService from '../db/DataDbService'; import ContextService, { IContext } from '../base/ContextService'; import TYPES from 'src/config/types';  export class DataViewService {     readonly loggerService = inject(TYPES.loggerService);     readonly dataDbService = inject(TYPES.dataDbService);      public findById = async (id: string, context: IContext) => {          return await ContextService.runInContext(async () => {              return await this.dataDbService.findById(id);          }, context);     }  }  export default DataViewService;

Самое важное в плохом коде — хорошее логирование. Для читаемости логов, нужно фиксировать serviceNameclientIduserId и requestId. Код не будет чистым, если передавать эти данные через аргументы функций, как минимум, стажеры обязательно где-нибудь потеряют одно из значений. Грамотно реализовать логирование можно через loggerService.logCtx, где переменная context берется из контекста исполнения async_hooks

Дополнительно, используя scoped сервисы, можно передавать токен пользователя для взаимодействия с интеграциями, например, Appwrite

Паттерн Generic Repository (BaseCRUD)

Код управления справочниками — базовый код. Если его повторять, гетерогенность системы повышается, приводит к ошибкам

common/BaseCRUD.ts

import { factory } from "di-factory"; import { Model } from "mongoose";  export const BaseCRUD = factory(     class {         constructor(public readonly TargetModel: Model) {}          public async create(dto: object) {             const passenger = await this.TargetModel.create(dto);             return passenger.toJSON();         }          public async findById(id: string) {             const passenger = await this.TargetModel.findById(id);             if (!passenger) {                 throw new Error(`${this.TargetModel.modelName} not found`);             }             return passenger.toJSON();         }          public async paginate(             filterData: object,             pagination: {                 limit: number;                 offset: number;             }         ) {             const documents = await this.TargetModel.find(filterData)                 .skip(pagination.offset)                 .limit(pagination.limit);             const total = await this.TargetModel.countDocuments(filterData);             return {                 rows: documents.map((item) => item.toJSON()),                 total: total,             };         }     } );  export default BaseCRUD;

services/db/DataDbService.ts

import { TBasePaginator } from "functools-kit"; import BaseCRUD from "src/common/BaseCRUD"; import { inject } from "src/core/di"; import {     DataModel,     Data,     DataRow,     DataFilterData, } from "src/schema/Data.schema"; import LoggerService from "../base/LoggerService"; import TYPES from "src/config/types";  export class DataDbService extends BaseCRUD(DataModel) {     public readonly loggerService = inject(TYPES.loggerService);      public create = async (dto: Data) => {         this.loggerService.logCtx(`dataDbService create`, { dto });         return await super.create(dto);     };      public findById = async (id: string) => {         this.loggerService.logCtx(`dataDbService findById`, { id });         return await super.findById(id);     }; }  export default DataDbService;

Проблема выделения общего кода состоит в том, что невозможно статически унаследовать класс без передачи динамического аргумента в конструкторе классе, даже если аргумент не меняется. В результате, конструктор пришлось бы переопределить, что в TypeScript создает неудобства с типизацией: унаследовать интерфейс от класса не получилось бы. Наследование со статическим аргументом позволяет избежать проблемы.

interface IDataPublicService extends DataPrivateService {}  export type TDataPublicService = {     [key in Exclude]: any; };  // Удобно для View сервиса, подсветит, что метод из Db сервиса не обернут class DataViewService implements TDataPublicService {  }

Stateless классы

Если необходимо написать аналитику по базе данных, стажер обязательно попытается выкачать базу в Array в поле класса. Чтобы ли чем объяснять, почему это неправильно, проще поставить задачу сразу писать такой класс как stateless: на каждый вызов метода создается обособленная инстанция класса, по завершению исполнения метода поля класса стираются.

import { stateless } from 'di-stateless';  const StatelessService = stateless(     class {         randomId = randomString();          constructor() {             console.log("StatelessService CTOR");         }          methodFoo = () => this.randomId;         methodBaz = () => this.randomId;         methodBar = () => this.randomId;          entry = () => console.log({             foo: this.methodFoo(),             bar: this.methodBar(),             baz: this.methodBaz(),         });     } );  const service = new TransientService();  service.entry(); // { foo: "ndjol", bar: "ndjol", baz: "ndjol" }  service.randomId = "not-random-id";  service.entry(); // { foo: "c2wiyf", bar: "c2wiyf", baz: "c2wiyf" }

Спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/articles/863342/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *