---
title: "Strategy Pattern no Angular: trocando regras sem espalhar switches"
description: "Um guia de produção sobre Strategy Pattern no Angular: quando um switch vira fronteira, como selecionar implementações com DI e como evitar indireção decorativa."
deck: "Strategy merece existir quando uma regra realmente varia por tenant, plano, rota, permissão ou política de runtime, e essa variação começa a vazar pela feature. A Gang of Four definiu Strategy em 1994 como 'uma família de algoritmos intercambiáveis, selecionados em runtime'. No Angular, o [`ErrorStateMatcher`](https://github.com/angular/components/blob/main/src/material/core/error/error-options.ts) no Material e o multi-provider [`HTTP_INTERCEPTORS`](https://angular.dev/guide/http/interceptors) entregam o pattern via DI token, sem switch espalhado pela base."
author: "André Ramos"
url: "https://andreramos.dev/pt/angular/strategy-pattern-in-angular/"
lang: "pt-BR"
type: "article"
---

# Strategy Pattern no Angular: trocando regras sem espalhar switches

> Strategy merece existir quando uma regra realmente varia por tenant, plano, rota, permissão ou política de runtime, e essa variação começa a vazar pela feature. A Gang of Four definiu Strategy em 1994 como 'uma família de algoritmos intercambiáveis, selecionados em runtime'. No Angular, o [`ErrorStateMatcher`](https://github.com/angular/components/blob/main/src/material/core/error/error-options.ts) no Material e o multi-provider [`HTTP_INTERCEPTORS`](https://angular.dev/guide/http/interceptors) entregam o pattern via DI token, sem switch espalhado pela base.

## Comece pela variação repetida

Não comece pelo nome do pattern. Comece pelo cheiro de review: a mesma decisão aparece no componente, template, validação, analytics e testes. Cada novo tenant ou plano adiciona outro `switch`, e cada switch fica um pouco diferente.

Eu costumo usar esse formato quando uma regra de produto começa a aparecer no componente, no template e nos testes com branches parecidos, mas não idênticos.

É aí que Strategy pode ajudar. A regra varia, o chamador quer um contrato estável e o DI do Angular pode selecionar a implementação em uma fronteira de rota, componente ou aplicação.

Se a regra aparece uma vez só, uma função provavelmente basta. Se a regra muda a cada tecla digitada, um computed talvez baste. Strategy é mais forte quando uma política estável precisa de implementações intercambiáveis.

| Variação | Candidato a Strategy? | Motivo |
|---|---|---|
| Preço por tipo de conta | Sim | Mesmo contrato de quote, regras e mensagens diferentes |
| Política de permissão por tenant | Sim | Mesmas checagens de ação, fonte de política diferente |
| Fluxo de checkout por rota | Sim | A rota pode selecionar a política antes da página rodar |
| Um condicional em um componente | Não | Um branch local é mais barato que indireção com DI |
| Formatação visual pura | Normalmente não | Um pipe ou formatter costuma ser mais claro |

## O cheiro: switches se espalham com a regra

Esta página de checkout tem uma fonte real de complexidade: política de preço. O componente calcula descontos, mensagens de revisão e totais. Parece inofensivo até o próximo plano adicionar uma nova exceção, ou o fluxo enterprise precisar de outra regra de revisão.

A duplicação não é só visual. O `switch` aparece em mais de um computed, então um branch pode ser atualizado enquanto outro fica com o comportamento antigo. Os testes também precisam saber qual branch aparece em qual computed.

O pattern se justifica quando o time consegue nomear a regra que varia. Aqui a regra não é estado de checkout em geral. É a política de preço para um tipo de conta selecionado.

*Política espalhada pela página*

```ts
type AccountKind = 'selfServe' | 'enterprise' | 'education';
type BillingCycle = 'monthly' | 'annual';

@Component({
  selector: 'app-checkout-page',
  templateUrl: './checkout-page.html',
})
export class CheckoutPage {
  readonly accountKind = input.required<AccountKind>();
  readonly seats = signal(12);
  readonly billingCycle = signal<BillingCycle>('annual');

  readonly subtotalCents = computed(() => this.seats() * 2900);

  readonly discountCents = computed(() => {
    switch (this.accountKind()) {
      case 'selfServe':
        return this.billingCycle() === 'annual'
          ? Math.round(this.subtotalCents() * 0.1)
          : 0;
      case 'enterprise':
        return Math.round(this.subtotalCents() * 0.2);
      case 'education':
        return Math.round(this.subtotalCents() * 0.5);
    }
  });

  readonly reviewMessage = computed(() => {
    switch (this.accountKind()) {
      case 'selfServe':
        return null;
      case 'enterprise':
        return 'A contract review is required before payment.';
      case 'education':
        return 'Eligibility must be verified before activation.';
    }
  });

  readonly totalCents = computed(() =>
    this.subtotalCents() - this.discountCents()
  );
}
```

## Extraia o contrato da política

Um contrato de strategy deve ser pequeno o suficiente para o chamador não precisar saber qual implementação recebeu. No checkout, a página envia um `CheckoutContext` e recebe um `CheckoutQuote`.

Neste artigo, política de preço é a regra de negócio que varia; strategy é o formato de implementação usado para trocar essa política com segurança.

Mantenha o contrato compartilhado sem drama. A variação fica dentro de cada strategy, não no chamador. Self-serve, enterprise e education agora podem mudar de forma independente sem adicionar outro branch ao componente.

As classes não são valiosas por serem classes. Elas são valiosas porque cada uma assume uma política que produto, QA e code review conseguem discutir pelo nome.

*Contrato da strategy de preço*

```ts
type BillingCycle = 'monthly' | 'annual';

interface CheckoutContext {
  seats: number;
  billingCycle: BillingCycle;
}

interface CheckoutQuote {
  subtotalCents: number;
  discountCents: number;
  totalCents: number;
  reviewMessage: string | null;
}

interface CheckoutPricingStrategy {
  quote(context: CheckoutContext): CheckoutQuote;
}

const seatPriceCents = 2900;

function baseQuote(context: CheckoutContext): CheckoutQuote {
  const subtotalCents = context.seats * seatPriceCents;

  return {
    subtotalCents,
    discountCents: 0,
    totalCents: subtotalCents,
    reviewMessage: null,
  };
}

@Injectable()
export class SelfServePricingStrategy implements CheckoutPricingStrategy {
  quote(context: CheckoutContext): CheckoutQuote {
    const quote = baseQuote(context);
    const discountCents = context.billingCycle === 'annual'
      ? Math.round(quote.subtotalCents * 0.1)
      : 0;

    return {
      ...quote,
      discountCents,
      totalCents: quote.subtotalCents - discountCents,
    };
  }
}

@Injectable()
export class EnterprisePricingStrategy implements CheckoutPricingStrategy {
  quote(context: CheckoutContext): CheckoutQuote {
    const quote = baseQuote(context);
    const discountCents = Math.round(quote.subtotalCents * 0.2);

    return {
      ...quote,
      discountCents,
      totalCents: quote.subtotalCents - discountCents,
      reviewMessage: 'A contract review is required before payment.',
    };
  }
}

@Injectable()
export class EducationPricingStrategy implements CheckoutPricingStrategy {
  quote(context: CheckoutContext): CheckoutQuote {
    const quote = baseQuote(context);
    const discountCents = Math.round(quote.subtotalCents * 0.5);

    return {
      ...quote,
      discountCents,
      totalCents: quote.subtotalCents - discountCents,
      reviewMessage: 'Eligibility must be verified before activation.',
    };
  }
}
```

## Selecione a strategy com DI do Angular

Interfaces TypeScript somem em runtime, então o Angular não consegue injetar `CheckoutPricingStrategy` diretamente. Use um `InjectionToken` como chave de runtime e forneça a implementação selecionada pelo sistema de providers do Angular.

Isso mantém a seleção fora do componente. A página pede `CHECKOUT_PRICING_STRATEGY`; a rota, componente ou configuração da app decide qual implementação satisfaz esse token.

A factory deve injetar apenas a implementação selecionada. Listar as classes em `providers` registra os provider records; o `switch` abaixo evita pedir ao Angular strategies que aquela rota não vai usar.

Não esconda estado de negócio dinâmico dentro do provider. Seleção por rota funciona bem quando rota, tenant, plano ou shell já decidem a política. Se o usuário pode trocar política dentro da tela, modele esse estado explicitamente em vez de fingir que DI é estado reativo.

*Provider da strategy*

```ts
type AccountKind = 'selfServe' | 'enterprise' | 'education';

export const CHECKOUT_PRICING_STRATEGY =
  new InjectionToken<CheckoutPricingStrategy>(
    'checkout.pricing.strategy'
  );

export function provideCheckoutPricingStrategy(
  kind: AccountKind
): Provider[] {
  return [
    SelfServePricingStrategy,
    EnterprisePricingStrategy,
    EducationPricingStrategy,
    {
      provide: CHECKOUT_PRICING_STRATEGY,
      useFactory: () => {
        switch (kind) {
          case 'selfServe':
            return inject(SelfServePricingStrategy);
          case 'enterprise':
            return inject(EnterprisePricingStrategy);
          case 'education':
            return inject(EducationPricingStrategy);
        }
      },
    },
  ];
}
```

*DI não é estado de tela*

```ts
const selectedKind = signal<AccountKind>('selfServe');

export const routes: Routes = [
  {
    path: 'checkout',
    // Avoid this when selectedKind can change inside the screen.
    providers: provideCheckoutPricingStrategy(selectedKind()),
    loadComponent: () =>
      import('./checkout/checkout-page.component')
        .then((m) => m.CheckoutPage),
  },
];
```

## Deixe a rota assumir política específica

Rotas Angular podem ter providers próprios, o que as torna uma boa fronteira para política que muda por rota. O componente de checkout continua igual; a rota decide se a página roda com preço self-serve, enterprise ou education.

Essa é a diferença entre Strategy e um switch espalhado. A decisão acontece uma vez na fronteira onde a variação entra na feature.

*Strategy selecionada pela rota*

```ts
export const checkoutRoutes: Routes = [
  {
    path: 'checkout/self-serve',
    providers: provideCheckoutPricingStrategy('selfServe'),
    loadComponent: () =>
      import('./checkout/checkout-page.component')
        .then((m) => m.CheckoutPage),
  },
  {
    path: 'checkout/enterprise',
    providers: provideCheckoutPricingStrategy('enterprise'),
    loadComponent: () =>
      import('./checkout/checkout-page.component')
        .then((m) => m.CheckoutPage),
  },
  {
    path: 'checkout/education',
    providers: provideCheckoutPricingStrategy('education'),
    loadComponent: () =>
      import('./checkout/checkout-page.component')
        .then((m) => m.CheckoutPage),
  },
];
```

## O componente consome um contrato

Depois que a strategy é selecionada, o componente fica menor sem perder comportamento. Ele ainda assume estado atual de UI: seats e billing cycle. Ele não assume política de preço.

Signals funcionam bem aqui porque a quote é estado derivado síncrono. O componente pode recomputar a quote quando inputs locais mudam, enquanto a strategy selecionada continua sendo uma dependência estável daquela instância de rota.

O template lê essa quote com `@let`, então o markup tem um nome local para o valor derivado e não precisa de non-null assertion na mensagem de aviso.

*Contrato da página de checkout*

```ts
@Component({
  selector: 'app-checkout-page',
  templateUrl: './checkout-page.html',
})
export class CheckoutPage {
  private readonly pricing = inject(CHECKOUT_PRICING_STRATEGY);

  readonly seats = signal(12);
  readonly billingCycle = signal<BillingCycle>('annual');

  private readonly context = computed(() => ({
    seats: this.seats(),
    billingCycle: this.billingCycle(),
  }));

  readonly quote = computed(() =>
    this.pricing.quote(this.context())
  );
}
```

*Contrato do template*

```html
@let currentQuote = quote();

<section class="checkout-summary">
  <p>Subtotal: {{ currentQuote.subtotalCents | cents }}</p>
  <p>Discount: {{ currentQuote.discountCents | cents }}</p>
  <strong>Total: {{ currentQuote.totalCents | cents }}</strong>

  @if (currentQuote.reviewMessage; as message) {
    <app-inline-warning [message]="message" />
  }
</section>
```

## Não transforme todo branch em strategy

O modo de falha é previsível: depois que o time vê Strategy funcionando, todo `if` começa a parecer pattern. Isso cria mais arquivos, mais providers e menos leitura direta.

Use Strategy quando ela dá uma casa explícita para uma regra variável. Evite quando a variação é local, temporária ou mais fácil de entender como expressão direta.

| Formato do código | Dono melhor | Motivo |
|---|---|---|
| Preço, permissão ou workflow por tenant | Strategy selecionada por DI | O chamador precisa de um contrato com regras intercambiáveis |
| Um branch local de UI | Componente ou template | A variação não é uma política reutilizável |
| Valor derivado síncrono de UI | `computed` | Signals já expressam derivação local |
| Formatação de valor para exibição | Pipe ou formatter | A regra é de apresentação, não uma fronteira de política |
| Tradução de DTO do backend | Adapter | O problema é tradução de contrato, não comportamento intercambiável |

## Teste a regra selecionada

Uma strategy deve baratear testes. Você pode testar a implementação selecionada pelo mesmo token que o componente injeta, ou substituir por uma strategy fake ao testar a página.

Este teste não renderiza checkout. Ele prova que a configuração de route/provider consegue selecionar a regra education e que a regra produz a quote esperada. Testes de componente podem focar em renderizar o contrato.

*Teste de seleção da strategy*

```ts
describe('checkout pricing strategy', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideCheckoutPricingStrategy('education')],
    });
  });

  it('applies the selected pricing rule through DI', () => {
    const strategy = TestBed.inject(CHECKOUT_PRICING_STRATEGY);

    const quote = strategy.quote({
      seats: 10,
      billingCycle: 'annual',
    });

    expect(quote.subtotalCents).toBe(29000);
    expect(quote.discountCents).toBe(14500);
    expect(quote.totalCents).toBe(14500);
    expect(quote.reviewMessage).toContain('Eligibility');
  });
});
```

## Artefato reaproveitável: Checklist de review para Strategy

- Crie uma strategy apenas quando uma regra real varia por trás de um contrato estável.
- Nomeie a política que varia antes de adicionar interfaces, tokens ou providers.
- Use um InjectionToken quando o chamador precisa injetar um contrato em formato de interface.
- Selecione strategies em fronteiras de rota, componente ou app quando a política é estável naquela fronteira.
- Não use DI como estado reativo quando o usuário pode trocar a política dentro da tela.
- Mantenha derivação local de UI em computed em vez de empurrar todo branch para DI.
- Teste a regra selecionada pelo mesmo token de provider que a feature consome.

## Fontes consultadas

- https://angular.dev/guide/di
- https://angular.dev/guide/di/dependency-injection-providers
- https://angular.dev/api/core/InjectionToken
- https://angular.dev/guide/http/interceptors
- https://github.com/angular/components/blob/main/src/material/core/error/error-options.ts
- https://www.angularspace.com/strategy-pattern-the-angular-way-di-and-runtime-flexibility/
- https://en.wikipedia.org/wiki/Design_Patterns

## Leia também

- [Adapter Pattern no Angular: isolando APIs e bibliotecas browser-only](https://andreramos.dev/pt/angular/adapter-pattern-in-angular/)
- [Facade Pattern no Angular: quando o componente sabe demais](https://andreramos.dev/pt/angular/facade-pattern-in-angular/)
- [Standalone sem transformar NgModules em vilões](https://andreramos.dev/pt/angular/standalone-without-ngmodule-dogma/)
