Você alguma vez já ouviu falar sobre o Padrão Facade no Angular ?
Recentemente me peguei em um desafio interessante sobre este tema, precisei mergulhar neste universo. Quero compartilhar com voces um poquinho sobre esse padrão. Bora lá ?
Os exemplos contidos neste artigo, são baseados em cenários reais👻
O que é Facade Pattern?
O Facade Pattern é um padrão de design estrutural, com o objetivo de simplificar a interação com o sistema. Em vez de expor a complexidade de diferentes componentes ou serviços, a Facade propõe uma interface simplificada, fazendo a ponte entre o cliente (eu, você desenvolvedor) e a aplicação.
Em outras palavras, a Facade funciona como uma "fachada" ou "porta de entrada" (daí o nome! rs) para um conjunto de funcionalidades complexas, a famosa regra de negocio da nossa aplicação. Ela esconde todos os detalhes de implementação e expõe apenas o necessário para que nós possamos usar, sem se preocupar com o que tá rolando por trás da cena.
Quando usar esse Padrão?
O Padrão Facade é particularmente útil varios cenários, onde gerenciar a complexidade e promover uma arquitetura limpa são preocupações-chave. Abaixo estão algumas situações onde esse padrão pode ser altamente benéfico:
- Sistemas complexos : Use quando seu aplicativo tiver vários subsistemas complexos. A Facade esconde essas complexidades fornecendo uma interface simples, tornando o código do cliente mais fácil de usar e entender.
- Dependências acopladas: se os componentes estiverem fortemente acoplados a vários serviços, uma Facade pode desvinculá-los, tornando a base de código mais flexível e fácil de manter.
- Organização do código : em grandes projetos, a Facade centraliza as interações com subsistemas em uma interface coesa, melhorando a legibilidade e a manutenção.
- Testabilidade: as Facades simplificam os testes permitindo que você simule uma única Facade em vez de vários serviços, tornando os testes mais confiáveis e menos complexos.
Resumindo galera, use esse padrão para simplificar interações complexas, reduzir o acoplamento e melhorar a capacidade de manutenção e testabilidade do seu código.
Por que usar o Facade Pattern no Angular?
No angular, muitas vezes a complexidade em interagir com múltiplos serviços, estados e componentes pode crescer de forma rápida. Imagine que, em um e-commerce, você tem serviços de carrinho de compras, gerenciamento de estoque, processamento de pagamento, e por aí vai… Cada um com suas próprias lógicas e interações. A Facade Pattern surge como uma maneira de centralizar todas essas interações em um único ponto de contato, simplificando a comunicação com os componentes.
Implementando o Facade Pattern no Angular
Tá perdido? Calma que o exemplo vai te salvar! Vem comigo 👇
Imagine que estamos construindo um e-commerce e precisamos de um carrinho de compras que interaja com um sistema de estoque, calcule o total do pedido e atualize a quantidade de produtos. Sem uma facade, os componentes precisariam se conectar diretamente a vários serviços, aumentando o acoplamento e a complexidade.
- Vamos criar nosso
CartService
, responsável por gerenciar a adição e remoção de produtos no carrinho.
@Injectable({
providedIn: 'root',
})
export class CartService {
private cartItems: CartItem[] = [];
getCartItems(): CartItem[] {
return this.cartItems;
}
addProductToCart(product: Product, quantity: number): void {
// lógica para adicionar ao carrinho
this.cartItems.push({ ...product, quantity });
}
removeProductFromCart(productId: string): void {
this.cartItems = this.cartItems.filter(item => item.id !== productId);
}
clearCart(): void {
this.cartItems = [];
}
}
- Vamos criar
InventoryService
que vai simular a disponibilidade do produto no estoque antes de adicionar ao carrinho.
@Injectable({
providedIn: 'root',
})
export class InventoryService {
checkProductAvailability(productId: string, quantity: number): Observable<boolean> {
// Aqui aconteceria a verificação de disponibilidade no estoque
return of(true);
}
}
- Por último o
PaymentService
processa o pagamento e finaliza a compra.
@Injectable({
providedIn: 'root',
})
export class PaymentService {
processPayment(totalAmount: number): Observable<string> {
// Aqui aconteceria a logica de pagamento
return of('Payment successful');
}
}
Antes de começarmos é importante que você veja como seria o nosso componente sem o Facade.
CartComponent
(Sem Facade)
import { Component, OnInit } from '@angular/core';
import { CartService } from '../services/cart.service';
import { InventoryService } from '../services/inventory.service';
import { PaymentService } from '../services/payment.service';
import { CartItem } from '../models/cart-item.model';
import { Product } from '../models/product.model';
import { catchError, of } from 'rxjs';
@Component({
selector: 'app-cart',
templateUrl: './cart.component.html',
})
export class CartComponent implements OnInit {
cartItems: CartItem[] = [];
message = '';
constructor(
private cartService: CartService,
private inventoryService: InventoryService,
private paymentService: PaymentService
) {}
ngOnInit(): void {
this.cartItems = this.cartService.getCartItems();
}
addProduct(product: Product, quantity: number): void {
// Checa disponibilidade no estoque antes de adicionar ao carrinho
this.inventoryService.checkProductAvailability(product.id, quantity).subscribe((isAvailable) => {
if (isAvailable) {
this.cartService.addProductToCart(product, quantity);
this.message = 'Produto adicionado ao carrinho com sucesso!';
this.cartItems = this.cartService.getCartItems(); // Atualiza o carrinho
} else {
this.message = 'Produto fora de estoque';
}
});
}
removeProduct(productId: string): void {
this.cartService.removeProductFromCart(productId);
// Atualiza o carrinho
this.cartItems = this.cartService.getCartItems();
}
checkout(): void {
const totalAmount = this.calculateTotal();
// Processo de pagamento
this.paymentService.processPayment(totalAmount).pipe(
catchError(() => {
this.message = 'Falha no pagamento. Tente novamente.';
return of(null);
})
).subscribe((result) => {
if (result) {
this.cartService.clearCart();
this.message = result;
this.cartItems = this.cartService.getCartItems(); // Limpa o carrinho
}
});
}
private calculateTotal(): number {
return this.cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
}
}
- Complexidade: Todo o código de verificação de disponibilidade, adição ao carrinho, e pagamento agora está diretamente no componente, tornando-o mais difícil de ler e entender.
- Dificil manter: Qualquer mudança na lógica de estoque, adição ao carrinho, ou checkout exigirá alterações no componente, tornando a manutenção mais complexa. Dificuldade nos Testes: Testar este componente se torna mais difícil, pois ele interage diretamente com múltiplos serviços. Isso requer a criação de mocks para cada serviço, e o gerenciamento de múltiplas assinaturas dos Observables.
Migrando agora para o Facade Pattern.
- Devemos criar o
CartFacade
, que será responsável pela interação entre os serviços listados acima.
import { Injectable } from '@angular/core';
import { CartService } from './cart.service';
import { InventoryService } from './inventory.service';
import { PaymentService } from './payment.service';
import { CartItem } from '../models/cart-item.model';
import { Product } from '../models/product.model';
import { Observable, of } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class CartFacade {
constructor(
private cartService: CartService,
private inventoryService: InventoryService,
private paymentService: PaymentService
) {}
getCartItems(): CartItem[] {
return this.cartService.getCartItems();
}
addProductToCart(product: Product, quantity: number): Observable<string> {
// Primeiro verifica a disponibilidade no estoque
return this.inventoryService.checkProductAvailability(product.id, quantity).pipe(
switchMap((isAvailable) => {
if (isAvailable) {
this.cartService.addProductToCart(product, quantity);
return of('Produto adicionado ao carrinho com sucesso!');
} else {
return of('Produto fora de estoque');
}
})
);
}
removeProductFromCart(productId: string): void {
this.cartService.removeProductFromCart(productId);
}
checkout(): Observable<string> {
const totalAmount = this.calculateTotal();
return this.paymentService.processPayment(totalAmount).pipe(
switchMap((result) => {
this.cartService.clearCart();
return of(result);
}),
catchError(() => of('Falha no pagamento. Tente novamente.'))
);
}
private calculateTotal(): number {
const cartItems = this.getCartItems();
return cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
}
}
Aqui, o CartFacade
faz o seguinte:
- Adiciona um produto ao carrinho: Antes de adicionar, verifica se o produto está disponível no estoque.
- Remove um produto do carrinho: Remove diretamente através do
CartService
. - Realiza o checkout: Calcula o total e utiliza o
PaymentService
para processar o pagamento.
CartComponent
com Facade:
import { Component, OnInit } from '@angular/core';
import { CartFacade } from '../facades/cart-facade.service';
import { Product } from '../models/product.model';
@Component({
selector: 'app-cart',
templateUrl: './cart.component.html',
})
export class CartComponent{
cartItems = this.cartFacade.getCartItems();
message: string = '';
constructor(private cartFacade: CartFacade) {}
addProduct(product: Product, quantity: number): void {
this.cartFacade.addProductToCart(product, quantity).subscribe((message) => {
this.message = message;
});
}
removeProduct(productId: string): void {
this.cartFacade.removeProductFromCart(productId);
}
checkout(): void {
this.cartFacade.checkout().subscribe((result) => {
this.message = result;
});
}
}
Note: Com esse padrão, o componente CartComponent
não precisa lidar com a lógica complexa de estoque, pagamento ou estado do carrinho. Ele apenas chama métodos simples expostos pela CartFacade, que oculta todas as interações com os diferentes serviços.
Conclusão
O Facade Pattern é uma abordagem muito poderosa para simplificar a estrutura do código em aplicações Angular, dependendo da complexidade da sua aplicação, o uso de Facades atenderá totalmente suas necessidades. Mas caso seja necessário gerenciar um alto volume de dados em situações complexas, considere o uso de uma biblioteca externa, como NgRx. Você pode usar Facades em conjunto com o NgRx.