Mão na massa: State

Problema:

A troca de estados de um objeto é um problema bastante comum. Tome como exemplo o personagem de um jogo, como o Mario. Durante o jogo acontecem várias trocas de estado com o Mario, por exemplo, ao pegar uma flor de fogo o mario pode crescer, se estiver pequeno, e ficar com a habilidade de soltar bolas de fogo.

Desenvolvendo um pouco mais o pensamento temos um conjunto grande de possíveis estados, e cada transição depende de qual é o estado atual do personagem. Como falado anteriormente, ao pegar uma flor de fogo podem acontecer quatro ações diferentes, dependendo de qual o estado atual do mario:

Se Mario pequeno -> Mario grande e Mario fogo
Se Mario grande -> Mario fogo
Se Mario fogo -> Mario ganha 1000 pontos
Se Mario capa -> Mario fogo

Todas estas condições devem ser checadas para realizar esta única troca de estado. Agora imagine o vários estados e a complexidade para realizar a troca destes estados: Mario pequeno, Mario grande, Mario flor e Mario pena.

Pegar Cogumelo:
Se Mario pequeno -> Mario grande
Se Mario grande -> 1000 pontos
Se Mario fogo -> 1000 pontos
Se Mario capa -> 1000 pontos

Pegar Flor:
Se Mario pequeno -> Mario grande e Mario fogo
Se Mario grande -> Mario fogo
Se Mario fogo -> 1000 pontos
Se Mario capa -> Mario fogo

Pegar Pena:
Se Mario pequeno -> Mario grande e Mario capa
Se Mario grande -> Mario capa
Se Mario fogo -> Mario fogo
Se Mario capa -> 1000 pontos

Levar Dano:
Se Mario pequeno -> Mario morto
Se Mario grande -> Mario pequeno
Se Mario fogo -> Mario grande
Se Mario capa -> Mario grande

Com certeza não vale a pena investir tempo e código numa solução que utilize várias verificações para cada troca de estado. Para não correr o risco de esquecer de tratar algum estado e deixar o código bem mais fácil de manter, vamos analisar como o padrão State pode ajudar.

State

A intenção do padrão:

“Permite a um objeto alterar seu comportamento quando seu estado interno muda. O objeto parecerá ter mudado de classe.” [1]

Pela intenção podemos ver que o padrão vai alterar o comportamento de um objeto quando houver alguma mudança no seu estado interno, como se ele tivesse mudado de classe.

Para implementar o padrão será necessário criar uma classe que contém a interface básica de todos os estados. Como definimos anteriormente o que pode causar alteração nos estados do objeto Mario, estas serão as operações básicas que vão fazer parte da interface.

public interface MarioState {
	MarioState pegarCogumelo();

	MarioState pegarFlor();

	MarioState pegarPena();

	MarioState levarDano();
}

Agora todos os estados do mario deverão implementar as operações de troca de estado. Note que cada operação retorna um objeto do tipo MarioState, pois como cada operação representa uma troca de estados, será retornado qual o novo estado o mario deve assumir.

Vejamos então uma classe para exemplificar um estado:

public class MarioPequeno implements MarioState {

	@Override
	public MarioState pegarCogumelo() {
		System.out.println("Mario grande");
		return new MarioGrande();
	}

	@Override
	public MarioState pegarFlor() {
		System.out.println("Mario grande com fogo");
		return new MarioFogo();
	}

	@Override
	public MarioState pegarPena() {
		System.out.println("Mario grande com capa");
		return new MarioCapa();
	}

	@Override
	public MarioState levarDano() {
		System.out.println("Mario morto");
		return new MarioMorto();
	}

}

Percebemos que a classe que define o estado é bem simples, apenas precisa definir qual estado deve ser trocado quando uma operação de troca for chamada. Vejamos agora outro exemplo de classe de estado:

public class MarioCapa implements MarioState {

	@Override
	public MarioState pegarCogumelo() {
		System.out.println("Mario ganhou 1000 pontos");
		return this;
	}

	@Override
	public MarioState pegarFlor() {
		System.out.println("Mario com fogo");
		return new MarioFogo();
	}

	@Override
	public MarioState pegarPena() {
		System.out.println("Mario ganhou 1000 pontos");
		return this;
	}

	@Override
	public MarioState levarDano() {
		System.out.println("Mario grande");
		return new MarioGrande();
	}

}

Bem simples não? Novos estados são adicionados de maneira bem simples. Vejamos então como seria o objeto que vai utilizar o estados, o Mario:

public class Mario {
	protected MarioState estado;
	
	public Mario() {
		estado = new MarioPequeno();
	}

	public void pegarCogumelo() {
		estado = estado.pegarCogumelo();
	}

	public void pegarFlor() {
		estado = estado.pegarFlor();
	}

	public void pegarPena() {
		estado = estado.pegarPena();
	}

	public void levarDano() {
		estado = estado.levarDano();
	}
}

A classe mario possui uma referência para um objeto estado, este estado vai ser atualizado de acordo com as operações de troca de estados, definidas logo em seguida. Quando uma operação for invocada, o objeto estado vai executar a operação e se atualizará automaticamente. Como exemplo de utilização, vejamos o seguinte código cliente:

public static void main(String[] args) {
	Mario mario = new Mario();
	mario.pegarCogumelo();
	mario.pegarPena();
	mario.levarDano();
	mario.pegarFlor();
	mario.pegarFlor();
	mario.levarDano();
	mario.levarDano();
	mario.pegarPena();
	mario.levarDano();
	mario.levarDano();
	mario.levarDano();
}

Por este código é possível avaliar todas as transições e todos os estados.

O diagrama UML a seguir resume visualmente as relações entre as classes:

Um pouco de teoria

Pelo visto no exemplo, o padrão é utilizado quando se precisa isolar o comportamento de um objeto, que depende de seu estado interno. O padrão elimina a necessidade de condicionais complexos e que frequentemente serão repetidos. Com o padrão cada “ramo” do condicional acaba se tornando um objeto, assim você pode tratar cada estado como se fosse um objeto de verdade, distribuindo a complexidade dos condicionais.

Incluir novos estados também é muito simples, basta criar uma nova classe e atualizar as operações de transição de estados. Com a primeira solução seriam necessários vários milhões de ifs novos e a alteração dos já existentes, além do grande risco de esquecer algum estado. Outra grande vantagem é que fica claro, com a estrutura do padrão, quais são os estados e quais são as possíveis transições.

O padrão State não define aonde as transições ocorrem, elas podem ser colocadas dentro das classes de estado ou dentro da classe que armazena o estado. No exemplo vimos que dentro de cada estado são definidos os novos objetos que são retornados. A principal vantagem desta solução é que fica mais simples adicionar os estados, cada novo estado define suas transições. O problema é que assim cada classe de estado precisa ter conhecimento sobre as outras subclasses, e se alguma delas mudar, é provável que a mudança se espalhe.

É comum que objetos estados obedeçam a outros dois padrões: Singleton e Flyweight. Estados singleton são capazes de manter informações, mesmo com as constantes trocas de estados. Estados Flyweight permitem o compartilhamento entre objetos que vão utilizar a mesma máquina de estado.

O padrão State tem semelhanças com outros dois padrões: Strategy e Bridge. Falando primeiro sobre o Strategy, note que a ideia é muito parecida: eliminar vários ifs complexos espalhados utilizando subclasses. A diferença básica é que o State é mais dinâmico que o Strategy, pois ocorrem várias trocas de objetos estados, os próprios objetos estados realizam as transições.

A semelhança com o padrão Bridge também pode ser notada facilmente pelo diagrama UML, no entanto a diferença está na intenção dos padrões. A intenção do Bridge é permitir que tanto a implementação quanto a interface possam mudar independentemente. No State a ideia é realmente mudar o comportamento de um objeto.

Código fonte completo

O código completo pode ser baixado no seguinte repositório Git: https://github.com/MarcosX/Padr-es-de-Projeto.

Os arquivos estão como um projeto do eclipse, então basta colocar no seu Workspace e fazer o import.

Se gostou do post compartilhe com seus amigos e colegas, senão, comente o que pode ser melhorado. Encontrou algum erro no código? Comente também. Possui alguma outra opinião ou alguma informação adicional? Comenta ai! 😀

Referências:

[1] GAMMA, Erich et al. Padrões de Projeto: Soluções reutilizáveis de software orientado a objetos.

Anúncios

10 comentários sobre “Mão na massa: State

  1. Adorei o código, vai me ajudar muito.. valeu! 😀

  2. Obrigado pelo exemplo simples…

  3. André Willik Valenti

    Gostei muito cara, parabéns! Vou usar para minhas aulas!

  4. Primeiramente, parabéns pelos posts, foram feitos com muito cuidado.
    Agora um questão. Quando rodei o exemplo, apareceu uma mensagem assim – “Mario Voadr”.
    E agora? Tive que olhar todos os estados a procura da mensagem errada.
    E se fossem dezenas de estados? E se não fosse uma simples letra omissa?
    Alguma sugestão? Devo usar algum padrão de projeto ou usar uma classe pública para strings que são constantes?
    Grato pela atenção.

    • Oi Ramar, obrigado pelo apoio.
      No exemplo eu deixei as strings “hard-coded” por simplicidade, mas o ideal seria extrair isso para uma classe, ou até mesmo um arquivo de configuração. Assim não precisaria repetir em vários lugares e facilitaria a busca pelo problema, como você relatou.

  5. Perfeito, simples e fácil de entender. Preciso fazer uma apresentação explicando o padrão state, e para isso preciso de 2 exemplos, e o seu é perfeito até mesmo para aqueles com mais dificuldades de entender. Parabéns.

  6. Post bastante interessante, com um exemplo legal.

    Só uma observação em relação à implementação. Na especificação do padrão, fica em aberto a opção de onde e como atualizar o estado (se dentro do próprio contexto ou dentro dos estados). Então, acabamos tendo várias formas de fazê-lo.

    Uma outra possibilidade é que a interface “StateBase” (MarioState, no exemplo) tenha nos seus métodos handle um parâmetro que é referência do contexto (no exemplo, a classe Mario) e não tem retorno, é void.
    Dessa forma, MarioState ficaria com o método: public void handle (Mario mario);
    Nas classes de estado concreto (MarioPequeno, por exemplo), a forma de implementação seria algo como:
    public void pegarPena (Mario mario){
    System.out.println(“Mario grande com capa”);
    mario.setEstado(new MarioCapa());
    }

    E na classe Mario, o método pegarPena ficaria assim:
    public void pegarPena() {
    estado.pegarPena(this);
    }

    Claro que o resultado, no fim das contas, acaba sendo o mesmo. Mas é uma outra abordagem.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s