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

ООП это про мусорные пакеты для плохого кода. Любой код становится плохим в длинной временной перспективе, однако, если обернуть его в интерфейс, он не воняет. Лучшее ООП реализовано в 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;
Самое важное в плохом коде — хорошее логирование. Для читаемости логов, нужно фиксировать serviceName, clientId, userId и 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/
Добавить комментарий