Nuss… E Agora?!?

26set/090

Marios, Máquinas de Estados e o Padrão de Projetos State – Parte 2

Antes de continuarem lendo, recomendo que dêem uma parada no artigo sobre o Princípio da Substituição de Liskov. Entender como podemos trocar uma classe mãe por uma filha (ou, no caso do State, uma Interface por uma Implementação) é quase todo o segredo por trás desse padrão de projetos e vai tornar muito maior o entendimento do que vem pela frente. E não se esqueçam de voltar para o State quando terminarem de ler sobre Liskov!

Para iniciar o State, comecemos encapsulando todos os métodos que identificamos no artigo anterior como alteradores de estados e propagar esses métodos para nossos estados.

"Não entendi nada, Tiago". É simples: colocando na interface os métodos em comum aos estados, farei com que cada um desses estados tenha obrigatoriamente aqueles métodos principais da lógica da máquina de estados. Falando neles, a lista compreende:

  • PegarCogumelo()
  • PegarEstrela()
  • PegarFlorDeFogo()
  • ReceberDano()

Nossa interface fica simples assim:

package Mario
{
	public interface IMario
	{
		function PegarCogumelo():IMario;
		function PegarEstrela():IMario;
		function PegarFlorDeFogo():IMario;
		function ReceberDano():IMario;
		function RetornarTipo():String;
	}
}

Vejam que todo método vai, obrigatoriamente, retornar um objeto IMario. Para debug, coloquei também um método extra, o “RetornarTipo()”. Ele pode ser chamado pelo código para ver em qual estado o Mario está naquele momento. Após a criação da interface, precisamos agora implementá-la, criando nossos estados. Para quem não se lembra, são:

  • Mario
  • Super Mario
  • Mario Flor de Fogo
  • Mario Invencível
  • Mario Morto

Cada um desses estados é uma classe que implementa nossa interface IMario. Vamos então criar um? Prestem bastante atenção ao código:

package Mario
{
	import Mario.IMario;

	public class TMario implements IMario
	{

		public function TMario(){}

		public function PegarCogumelo():IMario
		{
			trace("-- Mario pega um cogumelo!" +
				"Agora ele é um Super Mario --");
			return new TSuperMario();
		}

		public function PegarEstrela():IMario
		{
			trace("-- Mario pega um starman!" +
				"Agora ele é um Mario Invencível --");
			return new TMarioInvencivel();
		}

		public function PegarFlorDeFogo():IMario
		{
			trace("-- Mario pega uma fire flower!" +
				"Agora ele é um Mario Flor de Fogo --");
			return new TMarioFlorDeFogo();
		}

		public function ReceberDano():IMario
		{
			trace("-- Mario encosta em um" +
				"inimigo e morre! --");
			return new TMarioMorto();
		}

		public function RetornarTipo():String
		{
			return "normal";
		}
	}
}

Notem que cada método que faz Mario mudar de Estado ou forma retorna seu novo Estado. Lembram da IMario com seus métodos que retornam objetos IMario? Então... Como os métodos nela tem que ser padrão para todos os estados mas são implementados de forma diferente nesses estados, utilizamos o tal Princípio da Substituição de Liskov (você leu, né?). Não entendeu? Bem,  quando nosso método retorna um IMario estamos dizendo para o compilador que o método vai retornar um objeto IMario ou um objeto que implemente IMario. Utilizando esse princípio, os métodos de mudança de estado poderão retornar qualquer estado que implemente IMario.

Interessante, não? Nosso código fica extremamente manutenível, tornando a adição de um novo estado uma coisa muito fácil. Compare agora com o funcionamento de TSuperMario:

package Mario
{
	import Mario.IMario;

	public class TSuperMario implements IMario
	{
		public function TSuperMario(){}

		public function PegarCogumelo():IMario
		{
			trace("-- Super Mario pega um cogumelo," +
				"mas nada acontece com ele --");
			return this;
		}

		public function PegarFlorDeFogo():IMario
		{
			trace("-- Mario pega uma fire flower!" +
			        "Agora ele é um Mario Flor de Fogo --");
			return new TMarioFlorDeFogo();
		}

		public function PegarEstrela():IMario
		{
			trace("-- Super Mario pega uma estrela!" +
				"Agora ele é Mario Invencível --");
			return new TMarioInvencivel();
		}

		public function ReceberDano():IMario
		{
			trace("-- Super Mario encosta em um inimigo." +
				"Ele volta a ser Mario --");
			return new TMario();
		}

		public function RetornarTipo():String
		{
			return "super";
		}
	}
}

Viram? Alguns métodos (como PegarCogumelo() e ReceberDano() ), nesse estado, retornam objetos diferentes, mas que implementam a mesma interface! Temos até um return this significando que o objeto não mudou de estado naquele método, mas como this também implementa a interface IMario, não dá problema algum. Começaram a ver a tabela da 1ª parte sendo implementada, não?

Para poupar espaço, não colocarei o código de todos os estados. Basta ter a implementação de um para entendermos como as coisas estão acontecendo. Além disso, o projeto em Actionscript 3.0 está disponível para download no final do artigo. O importante agora é nos focarmos na nossa classe principal, a TMain.

public class TMain extends MovieClip
	{
		public function TMain()
		{
			var mario:IMario;

			//Qual é o estado inicial do Mario? ...
			// ... troque-o e veja.que o programa...
			// ... se comportará de forma...
			// ... completamente diferente!
			mario = new TMario();

			// O Mario (ainda no estado anterior)
			// pega um cogumelo
			mario = mario.PegarCogumelo();

			// O Mario (ainda no estado anterior) ...
			//  ... recebe dano
			mario = mario.ReceberDano();

			// O Mario (ainda no estado anterior) ...
			//  ... pega outro cogumelo
			mario = mario.PegarCogumelo();

			// O Mario (ainda no estado anterior) ...
			//  ...  pega uma Fire Flower
			mario = mario.PegarFlorDeFogo();

			// O Mario (ainda no estado anterior) ...
			//  ...  pega um starman
			mario = mario.PegarEstrela();

			// O Mario (ainda no estado anterior) ...
			//  ...  encosta em um inimigo
			mario = mario.ReceberDano();

			// O Mario (ainda no estado anterior) ...
			//  ...  encosta em um inimigo
			mario = mario.ReceberDano();

			// O Mario (ainda no estado anterior) ...
			//  ...  encosta em um inimigo
			mario = mario.ReceberDano();

			trace("-- No final, o Mario é um mario "
     				 + mario.RetornarTipo() +" --");
		}
	}
}

Na nossa main, basta criar um objeto do tipo inicial (no exemplo acima comecei o "jogo" como TMario) e, no decorrer do percurso, realizar as chamadas aos métodos respectivos. O código acima retorna para mim:

-- Mario pega um cogumelo! Agora ele é um Super Mario --
-- Super Mario encosta em um inimigo. Ele volta a ser Mario --
-- Mario pega um cogumelo! Agora ele é um Super Mario --
-- Mario pega uma fire flower! Agora ele é um Mario Flor de Fogo --
-- Mario pega um starman! Agora ele é um Mario Invencível --
-- Mario Invencível encosta em um inimigo, mas nada acontece com ele --
-- Mario Invencível encosta em um inimigo, mas nada acontece com ele --
-- Mario Invencível encosta em um inimigo, mas nada acontece com ele --
-- No final, o Mario é um mario invencível --

"Tá Tiago, esse trabalho todo prá q?" Esse trabalho todo traz os seguintes benefícios:

  1. O código fica muito mais simples de entender sem ifs e cases aninhados uns dentro dos outros.
  2. Simplifica a adição de um novo estado simplesmente fazendo com que ele implemente a interface base.
  3. Modifica o funcionamento do sistema sem que haja a necessidade de prévia programação.

"Não entendi a 3a coisa da lista, Tiago. Como assim 'modifica o funcionamento'?" Se você trocaro estado inicial ou a chamada a um método, toda a linha de funcionamento do programa poderá ser modificada com o mínimo esforço. Olha o que acontece, por exemplo, se eu não quiser que o Mario pegue a estrela:

-- Mario pega um cogumelo! Agora ele é um Super Mario --
-- Super Mario encosta em um inimigo. Ele volta a ser Mario --
-- Mario pega um cogumelo! Agora ele é um Super Mario --
-- Mario pega uma fire flower! Agora ele é um Mario Flor de Fogo --
-- Mario encosta em um inimigo! Ele vira um Mario --
-- Mario encosta em um inimigo e morre! --
-- No final, o Mario é um mario morto --

Ou então, mais engraçado, caso ele comece já invencível:

-- Mario Invencível pega um cogumelo! Ele continua sendo o Mario Invencível--
-- Mario Invencível encosta em um inimigo, mas nada acontece com ele --
-- Mario Invencível pega um cogumelo! Ele continua sendo o Mario Invencível--
-- Mario pega uma fire flower! Ele continua sendo o Mario Invencível --
-- Mario Invencível pega uma estrela, mas nada acontece com ele --
-- Mario Invencível encosta em um inimigo, mas nada acontece com ele --
-- Mario Invencível encosta em um inimigo, mas nada acontece com ele --
-- Mario Invencível encosta em um inimigo, mas nada acontece com ele --
-- No final, o Mario é um mario invencível --

Como podemos ver, nosso "jogo" está flexível para qualquer possível acontecimento no decorrer de uma fase: basta que o evento esteja previamente mapeado pela nossa máquina de estados. Existem outras implementações do padrão State, sendo a mais comum o encapsulamento do estado dentro da classe TMario. Essa implementação é um pouco mais complexa, mas faz com que trabalhemos o tempo todo com um objeto do tipo TMario (e não uma interface IMario que, no decorrer da execução do programa, muda seu tipo dependendo do estado atual). No fim, o resultado é o mesmo.

Na próxima parte, vou mostrar para vocês como fica o Diagrama de Transição de Estados disso aí. E, para fechar, não se esqueçam de pegar o código em Actionscript 3.0 aqui:

[download id="2"]