Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save leanfj/a402022c96497a03635648d9116af63e to your computer and use it in GitHub Desktop.
Save leanfj/a402022c96497a03635648d9116af63e to your computer and use it in GitHub Desktop.

Poderia me dar uma exemplo de valueObject de um produto em typescript

Claro, aqui está um exemplo simples de um Value Object em TypeScript para um produto:

class ProductId {
  private readonly id: string;

  constructor(id: string) {
    if (!id) {
      throw new Error('Product ID cannot be empty');
    }

    this.id = id;
  }

  public toString(): string {
    return this.id;
  }

  public equals(other: ProductId): boolean {
    return this.id === other.id;
  }
}

class Product {
  private readonly id: ProductId;
  private readonly name: string;
  private readonly price: number;

  constructor(id: ProductId, name: string, price: number) {
    if (!name) {
      throw new Error('Product name cannot be empty');
    }

    if (price <= 0) {
      throw new Error('Product price must be greater than 0');
    }

    this.id = id;
    this.name = name;
    this.price = price;
  }

  public getId(): ProductId {
    return this.id;
  }

  public getName(): string {
    return this.name;
  }

  public getPrice(): number {
    return this.price;
  }

  public equals(other: Product): boolean {
    return this.id.equals(other.id);
  }
}

Neste exemplo, ProductId é um value object que representa o ID de um produto, enquanto Product é uma entidade que representa um produto completo. O ProductId tem uma validação básica de que o ID não pode ser vazio e a classe contém um método equals para comparar IDs com outros objetos da mesma classe. Já a classe Product tem uma validação para o nome e preço, e também tem um método equals que compara produtos com base no seu ID.

o que seria uma modelagem rica para essa entidade product?

Uma modelagem rica para a entidade Product poderia incluir mais informações sobre o produto, além do nome e preço, que são as únicas informações consideradas na implementação anterior. Uma modelagem rica permite que a entidade Product tenha mais comportamentos e possa ser utilizada em mais casos de uso dentro do domínio da aplicação.

Aqui está um exemplo de como uma modelagem rica para a entidade Product poderia ser implementada em TypeScript:

class Product {
  private readonly id: ProductId;
  private readonly name: string;
  private readonly description: string;
  private readonly price: Money;
  private readonly category: Category;
  private readonly manufacturer: Manufacturer;
  private readonly images: ProductImage[];

  constructor(
    id: ProductId,
    name: string,
    description: string,
    price: Money,
    category: Category,
    manufacturer: Manufacturer,
    images: ProductImage[]
  ) {
    if (!name) {
      throw new Error('Product name cannot be empty');
    }

    if (price.getAmount() <= 0) {
      throw new Error('Product price must be greater than 0');
    }

    this.id = id;
    this.name = name;
    this.description = description;
    this.price = price;
    this.category = category;
    this.manufacturer = manufacturer;
    this.images = images;
  }

  public getId(): ProductId {
    return this.id;
  }

  public getName(): string {
    return this.name;
  }

  public getDescription(): string {
    return this.description;
  }

  public getPrice(): Money {
    return this.price;
  }

  public getCategory(): Category {
    return this.category;
  }

  public getManufacturer(): Manufacturer {
    return this.manufacturer;
  }

  public getImages(): ProductImage[] {
    return this.images;
  }

  public addImage(image: ProductImage): void {
    this.images.push(image);
  }

  public removeImage(image: ProductImage): void {
    const index = this.images.findIndex((i) => i.equals(image));

    if (index !== -1) {
      this.images.splice(index, 1);
    }
  }

  public equals(other: Product): boolean {
    return this.id.equals(other.id);
  }

  public changePrice(price: Money): void {
    if (price.getAmount() <= 0) {
      throw new Error('Product price must be greater than 0');
    }

    this.price = price;
  }
}

Nesta implementação, a entidade Product tem mais informações sobre o produto, como descrição, categoria, fabricante e imagens. Além disso, a entidade agora tem métodos para adicionar e remover imagens, e um método para alterar o preço do produto. Esses métodos permitem que a entidade tenha mais comportamentos e possa ser utilizada em mais casos de uso dentro do domínio da aplicação.

Note que a implementação acima ainda usa a classe ProductId como um Value Object para representar o ID do produto, e Money é uma outra classe Value Object que representa um valor monetário, enquanto Category, Manufacturer e ProductImage são outras entidades que também poderiam ser implementadas de forma rica.

pode gerar as classes do ultimo código de exemplo, Money, Category, Manufacture e ProductImage ?

Claro, aqui estão as classes Money, Category, Manufacturer e ProductImage que poderiam ser utilizadas na implementação rica da entidade Product:

class Money {
  private readonly amount: number;
  private readonly currency: string;

  constructor(amount: number, currency: string) {
    if (!currency) {
      throw new Error('Currency cannot be empty');
    }

    this.amount = amount;
    this.currency = currency;
  }

  public getAmount(): number {
    return this.amount;
  }

  public getCurrency(): string {
    return this.currency;
  }

  public equals(other: Money): boolean {
    return this.amount === other.amount && this.currency === other.currency;
  }
}

class Category {
  private readonly id: string;
  private readonly name: string;

  constructor(id: string, name: string) {
    if (!id) {
      throw new Error('Category ID cannot be empty');
    }

    if (!name) {
      throw new Error('Category name cannot be empty');
    }

    this.id = id;
    this.name = name;
  }

  public getId(): string {
    return this.id;
  }

  public getName(): string {
    return this.name;
  }

  public equals(other: Category): boolean {
    return this.id === other.id;
  }
}

class Manufacturer {
  private readonly id: string;
  private readonly name: string;

  constructor(id: string, name: string) {
    if (!id) {
      throw new Error('Manufacturer ID cannot be empty');
    }

    if (!name) {
      throw new Error('Manufacturer name cannot be empty');
    }

    this.id = id;
    this.name = name;
  }

  public getId(): string {
    return this.id;
  }

  public getName(): string {
    return this.name;
  }

  public equals(other: Manufacturer): boolean {
    return this.id === other.id;
  }
}

class ProductImage {
  private readonly url: string;

  constructor(url: string) {
    if (!url) {
      throw new Error('Product image URL cannot be empty');
    }

    this.url = url;
  }

  public getUrl(): string {
    return this.url;
  }

  public equals(other: ProductImage): boolean {
    return this.url === other.url;
  }
}

Na implementação acima, Money é um Value Object que representa um valor monetário, Category e Manufacturer são entidades que representam a categoria e o fabricante do produto, respectivamente, e ProductImage é um Value Object que representa uma imagem do produto. Todas essas classes possuem validações básicas em seus construtores, e implementam o método equals para comparar objetos da mesma classe.

Daria um exemplo de useCase de atualização dos dados de produto sendo a entrada para apenas a atualização de um propriedade dele como exemplo o nome ?

Claro! Aqui está um exemplo de caso de uso (use case) para atualizar apenas o nome de um produto:

import { ProductRepository } from "../repositories/ProductRepository";
import { Product } from "../models/Product";

export class UpdateProductNameUseCase {
  constructor(private productRepository: ProductRepository) {}

  async execute(productId: string, newName: string): Promise<Product> {
    const product = await this.productRepository.findById(productId);
    if (!product) {
      throw new Error("Product not found");
    }

    product.setName(newName);

    await this.productRepository.save(product);

    return product;
  }
}

Neste exemplo, estamos usando uma interface ProductRepository que representa um repositório de produtos, que pode ser implementado de diversas maneiras (ex: usando um banco de dados, uma API, um arquivo JSON, etc). O método execute recebe o ID do produto e o novo nome como parâmetros, busca o produto pelo ID usando o productRepository, atualiza o nome usando o método setName da classe Product, e em seguida, salva as alterações usando o productRepository. Se o produto não for encontrado, lançamos uma exceção informando que o produto não foi encontrado.

Observe que neste caso de uso estamos atualizando apenas o nome do produto, deixando as outras propriedades intactas.

como seria a implementação se a entrada pudesse varia entre um elemento ou vários ?

Se a entrada pudesse variar entre um elemento ou vários, podemos definir dois casos de uso diferentes para tratar cada um dos casos. Aqui está um exemplo de implementação para cada caso:

Caso de uso: Atualizar um único produto

import { ProductRepository } from "../repositories/ProductRepository";
import { Product } from "../models/Product";

export class UpdateProductNameUseCase {
  constructor(private productRepository: ProductRepository) {}

  async execute(productId: string, newName: string): Promise<Product> {
    const product = await this.productRepository.findById(productId);
    if (!product) {
      throw new Error("Product not found");
    }

    product.setName(newName);

    await this.productRepository.save(product);

    return product;
  }
}

Neste caso, estamos reutilizando o exemplo anterior de atualização de nome do produto. Neste caso, a entrada é um único produto identificado pelo seu ID, e o novo nome que deve ser atribuído a ele.

Caso de uso: Atualizar vários produtos de uma só vez

import { ProductRepository } from "../repositories/ProductRepository";
import { Product } from "../models/Product";

export class UpdateProductsUseCase {
  constructor(private productRepository: ProductRepository) {}

  async execute(productsToUpdate: Product[]): Promise<void> {
    for (const product of productsToUpdate) {
      const existingProduct = await this.productRepository.findById(product.getId());
      if (!existingProduct) {
        throw new Error(`Product ${product.getId()} not found`);
      }

      existingProduct.setName(product.getName());
      existingProduct.setDescription(product.getDescription());
      existingProduct.setPrice(product.getPrice());
      existingProduct.setCategory(product.getCategory());
      existingProduct.setManufacturer(product.getManufacturer());
      existingProduct.setImages(product.getImages());

      await this.productRepository.save(existingProduct);
    }
  }
}

Neste caso, a entrada é uma lista de produtos que devem ser atualizados. Para cada produto na lista, buscamos o produto no repositório usando o seu ID, e atualizamos todas as suas propriedades (nome, descrição, preço, categoria, fabricante e imagens) com as informações do produto fornecido na entrada. Observe que estamos lançando uma exceção caso um produto fornecido na entrada não exista no repositório.

Note que em ambos os casos de uso, estamos usando a mesma classe ProductRepository, que é injetada na classe do caso de uso no construtor, permitindo que diferentes implementações do repositório possam ser usadas sem modificar o código do caso de uso.

E se essa implementação for para um único produto mas as propriedades podem variar.

Se a entrada for um único produto, mas as propriedades a serem atualizadas podem variar, podemos criar um método na classe de caso de uso que aceita um objeto com as propriedades que devem ser atualizadas. Aqui está um exemplo de implementação:

import { ProductRepository } from "../repositories/ProductRepository";
import { Product } from "../models/Product";

export interface UpdateProductInput {
  id: string;
  name?: string;
  description?: string;
  price?: number;
  category?: Category;
  manufacturer?: Manufacturer;
  images?: ProductImage[];
}

export class UpdateProductUseCase {
  constructor(private productRepository: ProductRepository) {}

  async execute(input: UpdateProductInput): Promise<Product> {
    const product = await this.productRepository.findById(input.id);
    if (!product) {
      throw new Error("Product not found");
    }

    if (input.name) {
      product.setName(input.name);
    }
    if (input.description) {
      product.setDescription(input.description);
    }
    if (input.price) {
      product.setPrice(input.price);
    }
    if (input.category) {
      product.setCategory(input.category);
    }
    if (input.manufacturer) {
      product.setManufacturer(input.manufacturer);
    }
    if (input.images) {
      product.setImages(input.images);
    }

    await this.productRepository.save(product);

    return product;
  }
}

Neste caso, definimos uma interface UpdateProductInput que representa o objeto de entrada para a atualização de um produto, e incluímos todas as propriedades do produto que podem ser atualizadas (nome, descrição, preço, categoria, fabricante e imagens). Em seguida, na implementação do caso de uso, verificamos se cada propriedade é fornecida na entrada e, se sim, atualizamos o produto correspondente.

Observe que este exemplo assume que as propriedades que não foram fornecidas na entrada não devem ser atualizadas. Se você quiser garantir que todas as propriedades devem ser atualizadas, mesmo que algumas delas sejam nulas ou indefinidas, você pode ajustar a lógica do caso de uso de acordo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment