Quest-Finder / temvagamestre.server

MIT License
5 stars 0 forks source link

Refatoração 2.0 do back-end #76

Closed htamagnus closed 1 month ago

htamagnus commented 3 months ago

Como vocês sabem, o projeto já passou por um refactor nas minhas mãos uma vez, mas eu tentei fazer de forma rápida pra resolver uma dor que existia naquele momento, que era a complexidade enorme pra mexer no projeto. Por conta disso, foquei em "tirar o grosso" e resolver o resto depois. E esse momento chegou.

Minha proposta é seguir essa mesma arquitetura, porém bem refinada e simplificada, vamos aos pontos:

PROPOSTA:


POR QUE?

Factories e contracts adicionam camadas extras de abstração ao código. Para projetos menores ou de médio porte, essas camadas são desnecessárias e apenas aumentam a dificuldade de compreensão do fluxo do sistema (que é o que está acontecendo).

Ao remover essas camadas, o código fica mais direto e mais fácil de navegar, especialmente para quem não está familiarizados com o padrão.

Com menos arquivos e abstrações, o tempo gasto para implementar novas funcionalidades ou corrigir bugs diminui. Isso torna o desenvolvimento mais rápido por que podemos ir diretamente ao ponto. Teremos menos "saltos" entre arquivos (de um contract para um factory, depois para um use-case) facilita o trabalho no dia a dia.

A remoção dessas estruturas torna o código mais direto, onde é fácil entender o que cada parte faz sem a necessidade de navegar por várias camadas de abstração.


COMO fazer isso?

Com a refatoração ficaria mais ou menos assim:

Controller:

Controller (ProductsController): Define rotas para obter detalhes do produto e verificar a disponibilidade. Utiliza o serviço ProductsService para a lógica de negócios.

Vamos separar pelos contextos e as operações correspondentes, assim:

import { Controller, Get, Param } from '@nestjs/common';
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ProductDto } from './dto/product.dto';
import { ProductsService } from './products.service';

@ApiTags('products')
@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Get('/details/:productId')
  @ApiOperation({ summary: 'Get Product Details' })
  @ApiParam({ name: 'productId', required: true, description: 'The ID of the product' })
  @ApiResponse({ status: 200, description: 'Product details retrieved successfully', type: ProductDto })
  @ApiResponse({ status: 400, description: 'Invalid input data or Product not found' })
  @ApiResponse({ status: 404, description: 'Product not found' })
  async getProductDetails(@Param('productId') productId: string): Promise<ProductDto> {
    return await this.productsService.getProductDetails(productId);
  }

  @Get('/availability/:productId')
  @ApiOperation({ summary: 'Check Product Availability' })
  @ApiParam({ name: 'productId', required: true, description: 'The ID of the product' })
  @ApiResponse({ status: 200, description: 'Product availability status retrieved successfully' })
  @ApiResponse({ status: 400, description: 'Invalid input data' })
  @ApiResponse({ status: 404, description: 'Product not found' })
  async checkProductAvailability(@Param('productId') productId: string): Promise<{ available: boolean }> {
    return await this.productsService.checkProductAvailability(productId);
  }
}

Service:

O service contém a lógica de negócios e a interação com o banco de dados.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from '../common/entities/product.entity';
import { ProductDto } from './dto/product.dto';
import ProductNotFound from '../exceptions/HC005.product-not-found.error';
import { ProductRepository } from './repositories/product.repository';

@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(Product, 'store_database')
    private readonly productRepository: Repository<Product>,
    private readonly productRepository: ProductRepository
  ) {}

  async getProductDetails(productId: string): Promise<ProductDto> {
    const product = await this.productRepository.findOne({
      where: { id: productId }
    });

    if (!product) {
      throw ProductNotFound;
    }

    return {
      id: product.id,
      name: product.name,
      description: product.description,
      price: product.price,
      category: product.category,
      imageUrl: product.imageUrl,
      stockQuantity: product.stockQuantity,
      updatedAt: product.updatedAt
    };
  }

  async checkProductAvailability(productId: string): Promise<{ available: boolean }> {
    const product = await this.productRepository.findOne({
      where: { id: productId }
    });

    if (!product) {
      throw ProductNotFound;
    }

    return { available: product.stockQuantity > 0 };
  }
}

DTO:

com validaçÃO Zod:

import { createZodDto } from 'nestjs-zod';
import { z } from 'zod';

const productSchema = z.object({
  id: z.string(),
  name: z.string(),
  description: z.string(),
  price: z.number(),
  category: z.string(),
  imageUrl: z.string().nullable(),
  stockQuantity: z.number(),
  updatedAt: z.date()
});

export class ProductDto extends createZodDto(productSchema) {}

Module:

Configura o módulo para incluir o controlador e o serviço, além do repositório do produto.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Product } from '../common/entities/product.entity';
import { ProductsService } from './products.service';
import { ProductsController } from './products.controller';
import { ProductRepository } from './repositories/product.repository';

@Module({
  imports: [
    TypeOrmModule.forFeature([Product], 'store_database')
  ],
  controllers: [ProductsController],
  providers: [ProductsService, ProductRepository]
})
export class ProductsModule {}

Pastas:

src/
│
├── common/
│   └── entities/
│       └── product.entity.ts
│
├── exceptions/
│   └── HC005.product-not-found.error.ts
│
├── products/
│   ├── dto/
│   │   └── product.dto.ts
│   │
│   ├── repositories/
│   │   └── product.repository.ts
│   │
│   ├── products.controller.ts
│   ├── products.service.ts
│   └── products.module.ts
│
└── main.ts

Sintam-se a vontade para questionar e fazer perguntas, isso é uma visão MINHA, baseado em estudos e tendo em vista o que acho que vai ser melhor para todos e pro projeto. Se vocês acham que é necessário manter, comentem aqui também

htamagnus commented 3 months ago

@emerson-oliveira @felipesouza91 @clintonbrito Sintam-se a vontade para questionar e fazer perguntas, isso é uma visão MINHA, baseado em estudos e tendo em vista o que acho que vai ser melhor para todos e pro projeto. Se vocês acham que é necessário manter, comentem aqui também. Isso aqui não é uma verdade absoluta, e temos outras alternativas para esse refactor, que envolveria migrarmos para DDD também, se acharem necessário.

felipesouza91 commented 3 months ago

@htamagnus Poderíamos então usar direto o padrão MVC ( Model - Entidades, View - Controller, Controller-Services, Regas de Negocio e Repositórios ) do próprio NestJs, utilizando o sistema de injeção de dependência dele. Dessa forma ficaríamos muito próximo do que é apresentado no Start Guid do NestJS.

clintonbrito commented 3 months ago

@htamagnus Concordo com o que o @felipesouza91 mencionou. Quanto mais nos mantivermos próximos da "arquitetura original" do Nest, mais fácil será manter o projeto, especialmente quando novas pessoas, possivelmente com menos experiência (como eu), entrarem na equipe. Isso reduz a curva de aprendizado e permite que todos foquem no mais importante: entregar valor para o usuário final.

Como você explicou muito bem, essas camadas extras de abstração fazem mais sentido em projetos maiores do que o TVM. Assino embaixo acerca de iniciativas que reduzam a quantidade de pastas e arquivos, tornando a arquitetura mais simplificada.

Uma dúvida, @htamagnus : como ficaria em relação à sprint atual? Essa refatoração seria algo mais pra frente? Sigo com o modelo atual assim mesmo nessa sprint?

htamagnus commented 3 months ago

@htamagnus Concordo com o que o @felipesouza91 mencionou. Quanto mais nos mantivermos próximos da "arquitetura original" do Nest, mais fácil será manter o projeto, especialmente quando novas pessoas, possivelmente com menos experiência (como eu), entrarem na equipe. Isso reduz a curva de aprendizado e permite que todos foquem no mais importante: entregar valor para o usuário final.

Como você explicou muito bem, essas camadas extras de abstração fazem mais sentido em projetos maiores do que o TVM. Assino embaixo acerca de iniciativas que reduzam a quantidade de pastas e arquivos, tornando a arquitetura mais simplificada.

Uma dúvida, @htamagnus : como ficaria em relação à sprint atual? Essa refatoração seria algo mais pra frente? Sigo com o modelo atual assim mesmo nessa sprint?

SObre a priorização: primeiro as tasks da sprint, depois podemos seguir com as issues

htamagnus commented 3 months ago

Então podemos seguir assim, removendo essas camadas complexas de abstração e refatorando pra seguir o padrão MVC, o que acham?

@felipesouza91 @clintonbrito

felipesouza91 commented 3 months ago

Perfeito...

htamagnus commented 3 months ago

Minha sugestão: @felipesouza91 @clintonbrito

Com a refatoração ficaria mais ou menos assim:

Controller:

Controller (ProductsController): Define rotas para obter detalhes do produto e verificar a disponibilidade. Utiliza o serviço ProductsService para a lógica de negócios.

Vamos separar pelos contextos e as operações correspondentes, assim:

import { Controller, Get, Param } from '@nestjs/common';
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ProductDto } from './dto/product.dto';
import { ProductsService } from './products.service';

@ApiTags('products')
@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Get('/details/:productId')
  @ApiOperation({ summary: 'Get Product Details' })
  @ApiParam({ name: 'productId', required: true, description: 'The ID of the product' })
  @ApiResponse({ status: 200, description: 'Product details retrieved successfully', type: ProductDto })
  @ApiResponse({ status: 400, description: 'Invalid input data or Product not found' })
  @ApiResponse({ status: 404, description: 'Product not found' })
  async getProductDetails(@Param('productId') productId: string): Promise<ProductDto> {
    return await this.productsService.getProductDetails(productId);
  }

  @Get('/availability/:productId')
  @ApiOperation({ summary: 'Check Product Availability' })
  @ApiParam({ name: 'productId', required: true, description: 'The ID of the product' })
  @ApiResponse({ status: 200, description: 'Product availability status retrieved successfully' })
  @ApiResponse({ status: 400, description: 'Invalid input data' })
  @ApiResponse({ status: 404, description: 'Product not found' })
  async checkProductAvailability(@Param('productId') productId: string): Promise<{ available: boolean }> {
    return await this.productsService.checkProductAvailability(productId);
  }
}

Service:

O service contém a lógica de negócios e a interação com o banco de dados.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from '../common/entities/product.entity';
import { ProductDto } from './dto/product.dto';
import ProductNotFound from '../exceptions/HC005.product-not-found.error';
import { ProductRepository } from './repositories/product.repository';

@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(Product, 'store_database')
    private readonly productRepository: Repository<Product>,
    private readonly productRepository: ProductRepository
  ) {}

  async getProductDetails(productId: string): Promise<ProductDto> {
    const product = await this.productRepository.findOne({
      where: { id: productId }
    });

    if (!product) {
      throw ProductNotFound;
    }

    return {
      id: product.id,
      name: product.name,
      description: product.description,
      price: product.price,
      category: product.category,
      imageUrl: product.imageUrl,
      stockQuantity: product.stockQuantity,
      updatedAt: product.updatedAt
    };
  }

  async checkProductAvailability(productId: string): Promise<{ available: boolean }> {
    const product = await this.productRepository.findOne({
      where: { id: productId }
    });

    if (!product) {
      throw ProductNotFound;
    }

    return { available: product.stockQuantity > 0 };
  }
}

DTO:

com validaçÃO Zod:

import { createZodDto } from 'nestjs-zod';
import { z } from 'zod';

const productSchema = z.object({
  id: z.string(),
  name: z.string(),
  description: z.string(),
  price: z.number(),
  category: z.string(),
  imageUrl: z.string().nullable(),
  stockQuantity: z.number(),
  updatedAt: z.date()
});

export class ProductDto extends createZodDto(productSchema) {}

Module:

Configura o módulo para incluir o controlador e o serviço, além do repositório do produto.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Product } from '../common/entities/product.entity';
import { ProductsService } from './products.service';
import { ProductsController } from './products.controller';
import { ProductRepository } from './repositories/product.repository';

@Module({
  imports: [
    TypeOrmModule.forFeature([Product], 'store_database')
  ],
  controllers: [ProductsController],
  providers: [ProductsService, ProductRepository]
})
export class ProductsModule {}

Pastas:

src/
│
├── common/
│   └── entities/
│       └── product.entity.ts
│
├── exceptions/
│   └── HC005.product-not-found.error.ts
│
├── products/
│   ├── dto/
│   │   └── product.dto.ts
│   │
│   ├── repositories/
│   │   └── product.repository.ts
│   │
│   ├── products.controller.ts
│   ├── products.service.ts
│   └── products.module.ts
│
└── main.ts