Idiomas / Languages: Português (BR) · English
Repositório de estudo e demonstração do padrão Strategy aplicado a interfaces React. O objetivo é mostrar como encapsular comportamentos que variam (renderização, cálculos, integrações) em classes ou módulos intercambiáveis, mantendo o componente consumidor simples e estável.
- O que é o Strategy Pattern
- Quando usar no front-end
- Quando não usar
- Vantagens e desvantagens
- Strategy vs alternativas comuns
- Exemplos implementados neste projeto
- Estrutura do repositório
- Como executar
- Como estender
- Exemplos hipotéticos (não implementados)
- Referências
O Strategy define uma família de algoritmos (ou comportamentos), encapsula cada um em uma classe separada e os torna intercambiáveis. O cliente (no nosso caso, um Context ou o App.tsx) delega o trabalho para a estratégia ativa sem conhecer os detalhes da implementação.
flowchart LR
App["App.tsx"] --> Ctx["Context\n(ex.: CartPricingContext)"]
Ctx --> S1["StandardCartPricingStrategy"]
Ctx --> S2["BulkCartPricingStrategy"]
Ctx --> S3["BundleCartPricingStrategy"]
Ctx -.->|"escolhe uma"| S1
No front-end, “algoritmo” pode significar:
- Como renderizar um bloco de UI (promoções diferentes).
- Como calcular um valor (frete, total do carrinho).
- Como executar um fluxo com efeitos colaterais (notificação, pagamento).
O padrão não exige classes: em TypeScript, interfaces + funções ou objetos também funcionam. Este projeto usa classes + Context para deixar a analogia com design patterns clássicos explícita em material de estudo.
Use Strategy quando todas as condições abaixo forem verdadeiras (ou a maioria):
| Sinal | Exemplo no projeto |
|---|---|
| Existem várias variantes do mesmo comportamento | Bônus vs porcentagem; frete econômico vs expresso |
| A variante é escolhida em tempo de execução (tipo, config, feature flag) | discountType, optionType, CartPricingType |
| O consumidor deve permanecer estável ao adicionar variantes | App.tsx só chama promotionCardContext.renderDetails() |
| A lógica de cada variante cresce ou já é grande o suficiente para um módulo próprio | BundleCartPricingStrategy com combo + premium |
| Você quer testar cada regra isoladamente | Estratégias puras de cálculo sem montar a árvore React inteira |
- Variação de UI — cards, wizards, formulários com layout diferente por tipo.
- Regras de negócio no cliente — preço, frete, impostos, elegibilidade (prototipagem ou offline-first).
- Integrações plugáveis — gateway de pagamento, provedor de mapa, exportação de relatório.
- Efeitos colaterais por canal — toast, e-mail, push (cada canal = estratégia).
- Validação ou formatação — máscaras e regras por tipo de campo.
Evite Strategy (ou use algo mais simples) quando:
| Situação | Alternativa mais simples |
|---|---|
| Apenas 2 ramos estáveis e pequenos | if/else ou operador ternário |
| Só muda estilo (cor, tamanho) | Variantes de componente + className / CVA |
| A “estratégia” é uma linha | Função inline ou mapa de valores |
| Não há previsão de novas variantes | YAGNI — não abstraia cedo demais |
| Toda a equipe prefere composição React | Compound components, render props, hooks especializados |
O Strategy adiciona arquivos e indireção. O ganho aparece quando novas regras ou telas quebrariam um switch gigante no meio do JSX.
| Benefício | Por quê importa no front |
|---|---|
| Open/Closed | Nova promoção = nova classe + entrada no mapa do Context, sem reescrever App.tsx. |
| Responsabilidade única | EconomyShippingStrategy só conhece frete econômico; o card de checkout não acumula regras. |
| Testabilidade | calculateShippingCost(89.82) e calculateTotal() testáveis sem RTL em todo o fluxo. |
| Legibilidade | O seletor na UI mostra qual estratégia está ativa (StrategyFlow no demo). |
| Alinhamento com domínio | Nomes como BulkCartPricingStrategy conversam com produto/negócio. |
| Substituição em runtime | Troca de estratégia ao mudar estado (useState + novo Context). |
| Limitação | Mitigação |
|---|---|
| Mais arquivos e boilerplate | Manter interfaces enxutas; Context fino que só delega. |
| Curva de aprendizado | Documentar o mapa type → Strategy (como neste README). |
| Risco de over-engineering | Aplicar só quando há ≥3 variantes ou crescimento claro. |
| Estratégias com JSX | Dificulta testes unitários “puros”; separar cálculo em funções puras quando possível. |
| Não é “padrão React idiomático” | Hooks + composição são comuns; Strategy complementa, não substitui. |
| Instância por render | Usar useMemo no Context (como em App.tsx) para não recriar à toa. |
| Abordagem | Bom para | Limitação |
|---|---|---|
if/switch no componente |
1–2 variações locais | Cresce com o produto; JSX e regra misturados |
Mapa de componentes const X = { bonus: BonusCard } |
Variação só de UI | Cálculos acabam fora ou duplicados |
Hooks useCartPricing(type) |
Lógica sem classes | Pode virar hook “deus” com muitos switch internos |
| Compound components | Layout flexível | Não modela bem algoritmos de preço/frete |
| Strategy + Context | UI + regra + extensão frequente | Mais estrutura inicial |
Este repositório posiciona o Strategy como camada de domínio/apresentação entre os dados (get-cart.service.ts) e a UI (App.tsx).
A aplicação (src/App.tsx) expõe três contextos em duas seções didáticas. Em cada uma, o usuário troca a estratégia e vê o fluxo Context → Strategy na interface.
Problema: cartões de promoção com layouts e conteúdos diferentes (bônus “compre e ganhe” vs desconto percentual).
| Peça | Caminho |
|---|---|
| Interface | src/data/strategies/promotion-card-strategy/strategy.ts |
| Estratégias | bonus-promotion-strategy.tsx, percentage-promotion-strategy.tsx |
| Context | promotion-card-strategy/index.ts |
| Dados | src/data/services/get-promotion.service.ts |
Seleção da estratégia: promotion.discountType → bonus | percentage.
Métodos delegados:
renderProductInfo()— imagem e preço do produto principalrenderIcon()— ícone do tipo de promoçãorenderDetails()— bônus grátis ou barra de desconto %
Dimensão didática: Strategy focado em variação de UI (sem cálculo de pedido completo).
Problema: mesma área de checkout, mas cada modalidade tem copy, ícone e fórmula de frete próprios.
| Peça | Caminho |
|---|---|
| Interface | shipping-option-strategy/strategy.ts |
| Estratégias | economy-, express-, store-pickup-shipping-strategy.tsx |
| Context | shipping-option-strategy/index.ts |
| Pedido de referência | get-sample-order.service.ts (18 × R$ 4,99 = R$ 89,82) |
Seleção: shippingOption.optionType.
Métodos delegados:
renderName(),renderIcon(),renderDetails()calculateShippingCost(orderTotal)— regra de negócio
Regras de cálculo (resumo):
| Estratégia | Fórmula (pedido R$ 89,82) |
|---|---|
| Econômico | baseCost (4,99) + 1,5% do pedido → ~R$ 6,34 |
| Expresso | baseCost (12,99) + 3% do pedido → ~R$ 15,68 |
| Retirada | 0 se pedido ≥ R$ 50; senão R$ 5,99 → R$ 0 |
Dimensão didática: Strategy em UI + cálculo no mesmo contrato.
Problema: um único CartModel, três políticas de preço (padrão, volume, combo premium).
| Peça | Caminho |
|---|---|
| Interface | cart-pricing-strategy/strategy.ts |
| Estratégias | standard-, bulk-, bundle-cart-pricing-strategy.tsx |
| Context | cart-pricing-strategy/index.ts |
| Carrinho demo | get-cart.service.ts (subtotal R$ 101,85) |
| Utilitários | src/utils/cart.utils.ts (getCartSubtotal, roundMoney) |
Seleção: CartPricingType escolhido no seletor (standard | bulk | bundle).
Métodos delegados:
renderTitle(),renderDescription(),renderBreakdown()calculateTotal()
Totais esperados (carrinho atual):
| Estratégia | Regra principal | Total ~ |
|---|---|---|
| Padrão | 5% se subtotal ≥ R$ 100 | R$ 96,76 |
| Volume | 12% (≥5 un.) / 20% (≥10 un.) por item | R$ 86,66 |
| Combo | 15% em itens Coca+Fanta + 5% premium no subtotal | R$ 82,76 |
Dimensão didática: Strategy predominantemente de negócio, com UI de breakdown rica.
// 1. Estado escolhe o "tipo"
const [selectedCartPricing, setSelectedCartPricing] = useState<CartPricingType>("standard");
// 2. Dados de entrada
const cart = useMemo(() => getCart(), []);
// 3. Context resolve a estratégia (useMemo evita recriação desnecessária)
const cartPricingContext = useMemo(
() => new CartPricingContext(selectedCartPricing, cart),
[selectedCartPricing, cart]
);
// 4. UI só delega — não conhece Bulk vs Bundle
const total = cartPricingContext.calculateTotal();
// ...
{cartPricingContext.renderBreakdown()}O mesmo padrão se repete para PromotionCardContext e ShippingOptionContext.
src/
├── App.tsx # Demo: seletores + delegação aos Contexts
├── constants/strategy-labels.ts # Metadados didáticos (nomes Context/Strategy)
├── components/
│ ├── layout/ # StrategyFlow, StrategySection, GlassPanel
│ └── ui/ # Card, StrategySelector, ThemeToggle, …
├── data/
│ ├── models/ # Tipos de domínio (Promotion, Shipping, Cart)
│ ├── services/ # Dados mock (get-cart, get-promotion, …)
│ └── strategies/
│ ├── promotion-card-strategy/
│ ├── shipping-option-strategy/
│ └── cart-pricing-strategy/ # Cada pasta: strategy.ts, *-strategy.tsx, index.ts
└── utils/
├── cart.utils.ts # Subtotal e arredondamento monetário
├── date.utils.ts
└── number.utils.ts
Convenção por caso de uso:
strategy.ts— interface comum (PromotionCardStrategy, etc.).*-strategy.tsx— uma classe por variante.index.ts— Context comRecord<Tipo, Constructor>e métodos que delegam.
npm install
npm run devBuild de produção:
npm run build
npm run previewStack: Vite, React 18, TypeScript, Tailwind CSS.
Para adicionar um novo caso de uso ao demo:
- Criar tipos em
src/data/models/. - Definir a interface em
src/data/strategies/{caso}/strategy.ts. - Implementar cada variante em
*-strategy.tsx. - Registrar no mapa do Context em
index.ts. - Expor dados mock em
src/data/services/(se necessário). - Adicionar seletor e bloco na UI em
App.tsx+ entrada emstrategy-labels.ts.
O App.tsx deve continuar sem switch gigante sobre regras de negócio — apenas instancia o Context e chama a API pública dele.
Os blocos abaixo ilustram extensões naturais do mesmo desenho. Não estão no código para manter o demo focado; servem como exercício mental ou próximo passo.
Cenário: checkout com PIX, cartão e boleto. Cada um tem fluxo, validação e chamada de API diferentes.
// strategy.ts
export interface PaymentStrategy {
readonly id: "pix" | "card" | "billet";
validate(form: PaymentForm): ValidationResult;
submit(orderId: string, form: PaymentForm): Promise<PaymentResult>;
renderInstructions(): React.ReactNode;
renderFormFields(): React.ReactNode;
}
// index.ts — PaymentContext
const strategiesMap = {
pix: PixPaymentStrategy,
card: CardPaymentStrategy,
billet: BilletPaymentStrategy,
} satisfies Record<PaymentType, new (...args: never[]) => PaymentStrategy>;
export class PaymentContext {
constructor(type: PaymentType) {
this.strategy = new strategiesMap[type]();
}
submit(orderId: string, form: PaymentForm) {
return this.strategy.submit(orderId, form);
}
}Por que Strategy aqui: o CheckoutPage não precisa saber se o backend espera QR Code, token de cartão ou linha digitável.
Cenário: mesma mensagem de sucesso/erro enviada por toast, e-mail ou push — APIs e pré-requisitos distintos.
export interface NotificationPayload {
title: string;
body: string;
severity: "info" | "success" | "error";
}
export interface NotificationStrategy {
send(payload: NotificationPayload): Promise<void>;
}
export class ToastNotificationStrategy implements NotificationStrategy {
async send(payload: NotificationPayload) {
toast[payload.severity](payload.title, { description: payload.body });
}
}
export class EmailNotificationStrategy implements NotificationStrategy {
async send(payload: NotificationPayload) {
await fetch("/api/notifications/email", {
method: "POST",
body: JSON.stringify(payload),
});
}
}
export class NotificationContext {
constructor(private strategy: NotificationStrategy) {}
notify(payload: NotificationPayload) {
return this.strategy.send(payload);
}
}
// Uso no App — injeção por preferência do usuário ou feature flag
const notification = new NotificationContext(
user.prefersEmail ? new EmailNotificationStrategy() : new ToastNotificationStrategy()
);
await notification.notify({ title: "Pedido confirmado", body: "…", severity: "success" });Lição: Strategy não é só JSX — encapsula side effects com a mesma interface send().
Cuidado no React: para efeitos ligados ao ciclo de vida, combine com hooks (useNotification) que internamente escolhem a estratégia, evitando instanciar Context em todo render sem useMemo/factory.
Cenário: formulário com CPF, CNPJ, e-mail e CEP — regras e mensagens diferentes.
export interface FieldValidationStrategy {
normalize(value: string): string;
validate(value: string): { valid: boolean; message?: string };
}
export class CpfValidationStrategy implements FieldValidationStrategy {
normalize(value: string) {
return value.replace(/\D/g, "").slice(0, 11);
}
validate(value: string) {
// algoritmo de dígitos verificadores…
return { valid: isValidCpf(value) };
}
}O componente <ControlledInput strategy={cpfStrategy} /> permanece genérico.
Cenário: exportar o mesmo relatório em CSV, JSON ou PDF.
export interface ExportStrategy {
readonly mimeType: string;
readonly extension: string;
build(data: ReportRow[]): Blob | Promise<Blob>;
}
export class CsvExportStrategy implements ExportStrategy {
mimeType = "text/csv";
extension = "csv";
build(rows: ReportRow[]) {
const csv = [headers, ...rows.map(formatRow)].join("\n");
return new Blob([csv], { type: this.mimeType });
}
}Útil quando a estrutura dos dados é a mesma e só o serializador muda.
Cenário: “Entrar com Google / GitHub / Apple” — URLs, scopes e parsing de token diferentes.
Mesmo contrato: signIn(): Promise<Session>. O botão de login só escolhe a estratégia pelo ícone clicado.
| Context | Variante por | UI? | Side effect? | Complexidade típica |
|---|---|---|---|---|
| Payment | PaymentType |
Sim (formulário) | Sim (API) | Alta |
| Notification | canal / preferência | Opcional | Sim | Média |
| FieldValidation | tipo do campo | Não | Não | Baixa |
| ExportReport | formato | Não (download) | Sim (arquivo) | Média |
| AuthProvider | provedor OAuth | Sim (botão) | Sim | Alta |
Para aprender Strategy no front React, os três exemplos implementados são suficientes:
- Variação visual (promoções)
- Visual + cálculo (frete)
- Regras compostas (carrinho)
Adicione código novo só se quiser demonstrar um eixo extra (pagamento, notificação, validação) — ou documente hipotéticos como acima, sem inflar a UI do demo.
- Strategy — Refactoring Guru
- Design Patterns: Elements of Reusable Object-Oriented Software (GoF)
- Documentação do projeto: interfaces em
src/data/strategies/*/strategy.tse demo interativo emsrc/App.tsx
Projeto educacional — use e adapte livremente para estudos e apresentações.