Neste artigo
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.

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 atual | pay() | ship() | cancel() |
|---|---|---|---|
| pending | vai para paid | erro | vai para cancelled |
| paid | erro | vai para shipped | vai para cancelled |
| shipped | erro | erro | erro |
| cancelled | erro | erro | erro |
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
ifresolve 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ão | Quem escolhe o comportamento | As classes se conhecem? |
|---|---|---|
| State | O próprio objeto, conforme o estado em que está | Sim: um estado decide qual é o próximo |
| Strategy | O cliente, que entrega a estratégia pronta | Não: cada estratégia é independente |
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.

Continue a conversa