Design Pattern

Padrão de Projeto State em PHP com exemplo

Capa do artigo Padrão de Projeto State em PHP com exemplo
Leitura com foco

Entenda o contexto.
Leve para a prática.

Neste artigo
  1. Qual problema o State resolve?
  2. Exemplo do padrão State em PHP
  3. Testando o exemplo
  4. Quando usar o padrão State?
  5. Cuidados ao usar o State
  6. State e Strategy: qual a diferença?

O Padrão de Projeto State permite que um objeto mude de comportamento quando o seu estado interno muda. Para quem usa, parece que o objeto trocou de classe. Na prática, cada estado vira uma classe própria, e o objeto principal só repassa as chamadas para o estado em que está.

Com isso, as regras de cada estado ficam juntas em um lugar só, em vez de espalhadas em if e switch por todos os métodos.

Diagrama de classe UML do padrão de projeto State
Diagrama de classe UML do padrão de projeto State

Observe no diagrama os participantes do padrão:

  • Context (Order): o objeto que o cliente usa. Guarda o estado atual e delega para ele.
  • State (OrderState): a interface com as ações que dependem do estado.
  • ConcreteState (PendingState, PaidState, ShippedState e CancelledState): cada classe implementa o comportamento de um estado e decide para qual estado o pedido vai em seguida.

Qual problema o State resolve?

Pense em um pedido de loja virtual. Ele nasce pendente, é pago, é enviado, e pode ser cancelado no meio do caminho. Só que nem tudo vale em qualquer momento: não dá para enviar um pedido que não foi pago, nem cancelar um pedido que já saiu para entrega.

A primeira solução que vem à cabeça é guardar o status em um texto e testar esse texto em cada método:

public function ship(): void
{
    if ($this->status === 'pending') {
        throw new LogicException('Order must be paid before shipping');
    }

    if ($this->status === 'shipped') {
        throw new LogicException('Order already shipped');
    }

    if ($this->status === 'cancelled') {
        throw new LogicException('Order is cancelled');
    }

    $this->status = 'shipped';
}

Funciona, mas esse mesmo bloco se repete no pay(), no cancel() e em cada método novo. Quando surgir um estado a mais, você precisa lembrar de mexer em todos eles. Com o State, cada estado é uma classe, e incluir um estado novo é criar uma classe nova.

As regras do nosso pedido são estas:

Estado atualpay()ship()cancel()
pendingvai para paiderrovai para cancelled
paiderrovai para shippedvai para cancelled
shippederroerroerro
cancellederroerroerro
O que cada ação faz em cada estado do pedido.

Exemplo do padrão State em PHP

Vamos para o código. A interface diz o que todo estado precisa saber responder:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\State;

interface OrderState
{
    public function name(): string;

    public function pay(Order $order): void;

    public function ship(Order $order): void;

    public function cancel(Order $order): void;
}

O Order é o contexto. Repare que ele não tem nenhum if: só guarda o estado atual e repassa a chamada. O método changeState() é usado pelos próprios estados para fazer a transição.

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\State;

final class Order
{
    private OrderState $state;

    public function __construct()
    {
        $this->state = new PendingState();
    }

    public function changeState(OrderState $state): void
    {
        $this->state = $state;
    }

    public function status(): string
    {
        return $this->state->name();
    }

    public function pay(): void
    {
        $this->state->pay($this);
    }

    public function ship(): void
    {
        $this->state->ship($this);
    }

    public function cancel(): void
    {
        $this->state->cancel($this);
    }
}

Agora os estados. O pedido pendente pode ser pago ou cancelado, mas não pode ser enviado:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\State;

use LogicException;

final class PendingState implements OrderState
{
    public function name(): string
    {
        return 'pending';
    }

    public function pay(Order $order): void
    {
        $order->changeState(new PaidState());
    }

    public function ship(Order $order): void
    {
        throw new LogicException('Order must be paid before shipping');
    }

    public function cancel(Order $order): void
    {
        $order->changeState(new CancelledState());
    }
}

O pedido pago pode ser enviado ou cancelado:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\State;

use LogicException;

final class PaidState implements OrderState
{
    public function name(): string
    {
        return 'paid';
    }

    public function pay(Order $order): void
    {
        throw new LogicException('Order already paid');
    }

    public function ship(Order $order): void
    {
        $order->changeState(new ShippedState());
    }

    public function cancel(Order $order): void
    {
        $order->changeState(new CancelledState());
    }
}

Depois de enviado, o pedido não aceita mais nenhuma das três ações:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\State;

use LogicException;

final class ShippedState implements OrderState
{
    public function name(): string
    {
        return 'shipped';
    }

    public function pay(Order $order): void
    {
        throw new LogicException('Order already paid');
    }

    public function ship(Order $order): void
    {
        throw new LogicException('Order already shipped');
    }

    public function cancel(Order $order): void
    {
        throw new LogicException('Shipped order cannot be cancelled');
    }
}

E o pedido cancelado também é um estado final:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\State;

use LogicException;

final class CancelledState implements OrderState
{
    public function name(): string
    {
        return 'cancelled';
    }

    public function pay(Order $order): void
    {
        throw new LogicException('Order is cancelled');
    }

    public function ship(Order $order): void
    {
        throw new LogicException('Order is cancelled');
    }

    public function cancel(Order $order): void
    {
        throw new LogicException('Order is cancelled');
    }
}

Cada linha daquela tabela virou uma classe. Para saber o que um pedido pago pode fazer, basta abrir o PaidState.

Testando o exemplo

Os testes percorrem o caminho normal do pedido e as transições proibidas:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Tests\Behavioral\State;

use Growthdev\DesignPatterns\Behavioral\State\Order;
use LogicException;
use PHPUnit\Framework\TestCase;

final class StateTest extends TestCase
{
    public function testExpectOrderToGoFromPendingToShipped(): void
    {
        $order = new Order();
        $this->assertSame('pending', $order->status());

        $order->pay();
        $this->assertSame('paid', $order->status());

        $order->ship();
        $this->assertSame('shipped', $order->status());
    }

    public function testExpectPendingOrderNotToBeShipped(): void
    {
        $this->expectException(LogicException::class);
        $this->expectExceptionMessage('Order must be paid before shipping');

        $order = new Order();
        $order->ship();
    }

    public function testExpectPaidOrderToBeCancelled(): void
    {
        $order = new Order();
        $order->pay();
        $order->cancel();

        $this->assertSame('cancelled', $order->status());
    }

    public function testExpectShippedOrderNotToBeCancelled(): void
    {
        $this->expectException(LogicException::class);
        $this->expectExceptionMessage('Shipped order cannot be cancelled');

        $order = new Order();
        $order->pay();
        $order->ship();
        $order->cancel();
    }

    public function testExpectCancelledOrderToRejectPayment(): void
    {
        $this->expectException(LogicException::class);
        $this->expectExceptionMessage('Order is cancelled');

        $order = new Order();
        $order->cancel();
        $order->pay();
    }
}

Repare que os testes só conversam com o Order. Eles chamam pay(), ship() e cancel() sem saber que existem classes de estado por trás, e mesmo assim o comportamento muda a cada passo.

Quando usar o padrão State?

  • Quando o comportamento de um objeto depende do estado dele e muda em tempo de execução.
  • Quando os mesmos testes de status se repetem em vários métodos da classe.
  • Quando as regras de transição mudam com frequência ou novos estados aparecem: pedidos, pagamentos, assinaturas, documentos com fluxo de aprovação.

Cuidados ao usar o State

  • O número de classes cresce: são quatro estados, quatro classes. Para dois estados e uma regra, um if resolve melhor.
  • Os estados se conhecem, porque um decide qual é o próximo. Se as transições ficarem difíceis de acompanhar, vale desenhar a tabela antes de programar.
  • Neste exemplo cada transição cria um objeto de estado novo. Como os estados não guardam dados, isso não é um problema; se guardassem, seria preciso decidir quem cuida desses dados.

State e Strategy: qual a diferença?

Os dois padrões têm a mesma estrutura: um objeto principal que delega para outro objeto, trocável. A diferença está em quem faz a troca e por quê:

PadrãoQuem escolhe o comportamentoAs classes se conhecem?
StateO próprio objeto, conforme o estado em que estáSim: um estado decide qual é o próximo
StrategyO cliente, que entrega a estratégia prontaNão: cada estratégia é independente
Diferença entre State e Strategy.

Esse é mais um padrão comportamental do livro GoF. Para conhecer os outros, veja o artigo Resumo dos Padrões de Projetos.

O código completo deste exemplo, com os testes, está no meu GitHub, na pasta src/Behavioral/State do repositório growthdev-repo/design-patterns.

Veja também: o resumo dos 23 padrões de projeto GoF e os outros padrões comportamentais já publicados: Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, Strategy, Template Method, Visitor.

Confiança Sempre!!!

Fontes:

  • [1] GAMMA, Erich et al. Padrões de Projeto: Soluções reutilizáveis de software orientado a objetos.
  • [2] FREEMAN, Eric; ROBSON, Elisabeth; BATES, Bert; SIERRA, Kathy. Head First Design Patterns.
Sobre quem escreve

Walmir Silva

Olá! Sou Walmir, engenheiro de software com MBA em Engenharia de Software e o cérebro por trás do GrowthCode e autor do livro "Além do Código".

Se você acha que programação é apenas sobre escrever código, prepare-se para expandir seus horizontes. Aqui, nós vamos além do código e exploramos as interseções fascinantes entre tecnologia, negócios, artes e filosofia.

Você está em busca de crescimento na carreira? Quer se destacar em um mercado competitivo? Almeja uma vida mais rica em conhecimento e realização? Então você chegou ao lugar certo. No GrowthCode, oferecemos insights profundos, estratégias comprovadas e um toque de sabedoria filosófica para catalisar seu crescimento pessoal e profissional.

Ver todos os artigos →

Continue a conversa

Compartilhe sua perspectiva

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Encontre sua próxima resposta

O que você quer aprender?

Preferências de cookies

Os cookies opcionais começam desligados. Você pode mudar a escolha a qualquer momento por esta tela.

NecessáriosSempre ativos

Guardam a sua escolha de cookies e mantêm o site funcionando. Não são usados para publicidade.

Contam visitas e mostram como o site é usado: Google Analytics e Microsoft Clarity, carregados pelo Google Tag Manager. Como esse contêiner também carrega tags de publicidade, as estatísticas só são ativadas junto com Publicidade e marketing.

Exibem anúncios do Google AdSense e medem campanhas e e-mails: pixels da Meta e do TikTok e o Mautic.

Permite que o Google escolha anúncios com base nos seus interesses. Desligado, os anúncios aparecem sem personalização. Depende de Publicidade e marketing.

Também é possível bloquear cookies nas configurações do navegador. Detalhes na Política de privacidade.