Pular para o conteúdo principal

Desenvolvendo um App com React Native e DDD

· Leitura de 4 minutos
Thiago Sciotta
Front End Engineer

O desenvolvimento de aplicativos móveis usando React Native oferece agilidade e flexibilidade. No entanto, para garantir que o código seja escalável e fácil de manter, é importante aplicar boas práticas de design de software, como o Domain-Driven Design (DDD).

Neste artigo, vamos explorar como aplicar DDD em um projeto React Native usando TypeScript.

Estruturando o Projeto com DDD

1. Identificando o Domínio Principal

Antes de iniciar o desenvolvimento, entenda o domínio do problema que seu aplicativo resolverá. Suponha que estamos criando um app de e-commerce. Neste caso, entidades como "Produto", "Cliente" e "Pedido" seriam essenciais.

2. Camada de Domínio

Nesta camada, você define as regras de negócio principais, dividindo-as em entidades, value objects, repositórios e serviços de domínio.

Exemplo de Entidade:

// src/domain/entities/Product.ts
export class Product {
constructor(
public readonly id: string,
public name: string,
public price: number
) {}

changePrice(newPrice: number) {
if (newPrice <= 0) {
throw new Error("Price must be greater than zero.");
}
this.price = newPrice;
}
}

Exemplo de Value Object:

// src/domain/value-objects/Address.ts
export class Address {
constructor(
public readonly street: string,
public readonly city: string,
public readonly postalCode: string
) {}
}

Exemplo de Repositório:

// src/domain/repositories/ProductRepository.ts
export interface ProductRepository {
findById(id: string): Promise<Product | null>;
save(product: Product): Promise<void>;
}

Exemplo de Serviço de Domínio:

// src/domain/services/ProductService.ts
export class ProductService {
constructor(private productRepository: ProductRepository) {}

async updateProductPrice(id: string, newPrice: number): Promise<void> {
const product = await this.productRepository.findById(id);
if (!product) throw new Error("Product not found");
product.changePrice(newPrice);
await this.productRepository.save(product);
}
}

3. Camada de Aplicação

Os casos de uso do aplicativo são definidos nesta camada. Eles são os "application services" que orquestram as interações entre as entidades e serviços do domínio.

Exemplo de Caso de Uso:

// src/application/use-cases/AddProductToCart.ts
import { Cart } from "../entities/Cart";
import { ProductRepository } from "../../domain/repositories/ProductRepository";

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

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

4. Camada de Infraestrutura

Esta camada lida com as implementações concretas de persistência, APIs e outras infraestruturas necessárias.

Exemplo de Implementação de Repositório:

// src/infrastructure/repositories/InMemoryProductRepository.ts
import { ProductRepository } from "../../domain/repositories/ProductRepository";
import { Product } from "../../domain/entities/Product";

export class InMemoryProductRepository implements ProductRepository {
private products: Product[] = [];

async findById(id: string): Promise<Product | null> {
return this.products.find(product => product.id === id) || null;
}

async save(product: Product): Promise<void> {
const index = this.products.findIndex(p => p.id === product.id);
if (index !== -1) {
this.products[index] = product;
} else {
this.products.push(product);
}
}
}

5. Camada de Interface do Usuário

Nesta camada, os componentes React Native são definidos. Eles interagem com os casos de uso e exibem os dados para o usuário.

Exemplo de Componente:

// src/ui/components/ProductCard.tsx
import React from 'react';
import { View, Text, Button } from 'react-native';
import { Product } from '../../domain/entities/Product';

interface ProductCardProps {
product: Product;
onAddToCart: () => void;
}

export const ProductCard: React.FC<ProductCardProps> = ({ product, onAddToCart }) => {
return (
<View>
<Text>{product.name}</Text>
<Text>{product.price}</Text>
<Button title="Add to Cart" onPress={onAddToCart} />
</View>
);
};

6. Testes

Os testes garantem que a lógica de negócio funcione conforme o esperado. Abaixo, vemos um exemplo de teste para o serviço de domínio ProductService.

Exemplo de Teste com Jest:

// src/domain/services/ProductService.test.ts
import { ProductService } from './ProductService';
import { InMemoryProductRepository } from '../../infrastructure/repositories/InMemoryProductRepository';
import { Product } from '../entities/Product';

describe('ProductService', () => {
it('should update the product price', async () => {
const productRepository = new InMemoryProductRepository();
const productService = new ProductService(productRepository);
const product = new Product('1', 'Test Product', 100);

await productRepository.save(product);
await productService.updateProductPrice('1', 200);

const updatedProduct = await productRepository.findById('1');
expect(updatedProduct?.price).toBe(200);
});
});

Próximos passos

Aplicar DDD em um projeto React Native e TypeScript pode parecer complexo inicialmente, mas proporciona um código mais organizado, alinhado com o domínio e mais fácil de manter. Com esta estrutura, seu aplicativo estará melhor preparado para crescer e se adaptar às necessidades do negócio ao longo do tempo.

Em um próximo artigo, disponibilizarei um repositório preparado com um projeto base seguindo esse artigo.