Facade Pattern no Angular

Mateus Oliveira - Nov 6 - - Dev Community

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.

  1. 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 = [];
  }
}

Enter fullscreen mode Exit fullscreen mode
  1. 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);
  }
}

Enter fullscreen mode Exit fullscreen mode
  1. 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');
  }
}

Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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.

  1. 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);
  }
}

Enter fullscreen mode Exit fullscreen mode

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;
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

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.

.
Terabox Video Player