Você já deve ter ouvido falar muito sobre este design pattern, existem frameworks em outras linguagens que você trabalha com MVC “sem perceber”, que é o caso do JSF, Django, RoR… Pois não há outra saída a não ser “seguir o fluxo”.
Já no caso do flash encontramos alguns problemas que acabam atrapalhando a aplicação deste conceito:
- As frameworks MVC AS3 não garantem que você irá utilizar o modelo MVC nos seus projetos, pois você pode escrever qualquer bloco de código que faça o papel de outra camada no lugar errado.
- Poucos exemplos na internet, afinal, podemos considerar que o uso de frameworks MVC é uma forma alternativa de se programar um site ou aplicativo em as3.
Portanto a aplicação dos conceitos acaba não se tornando transparente como em algumas outras linguagems e se tornando muito trabalhoso. Mas vamos lá:
PureMVC vs Robotlegs
Ambas são boas, muitos gostam de defender determinada framework ou linguagem mas está mais que provado que isto não é motivo de discussão, todas possuem seus altos e baixos assim como possuem suas particularidades:
PureMVC:
Não é restrito apenas para AS3, se expandiu para diversas outras linguagens. No AS3 considero sua principal característica o uso intensivo das Notifications (lembra um pouco o Dispatch Event convencional), é possível comunicar um Mediator com outro usando apenas via Notification que dependendo de seu projeto pode se tornar um alívio de burocracia mas em outros casos o princípio de uma possível desorganização.
A vantagem que vejo no PureMVC é que seu desenvolvimento é um pouco menos trabalhoso que o Robotlegs, pois um Switch/Case é capaz de tratar todas as Notifications disparas dentro da Façade. Na minha opinião em relação escalabilidade x complexidade o PureMVC é ideal para pequenos projetos.
Robotlets
Este se tornou o favorito no nicho dos desenvolvedores AS3, é um pouco mais “burocrático” em relação ao PureMVC, pois o uso de Commands é indispensável já que não possui Notificators, tudo é a base de delegação de eventos da framework.
Possui injeção de dependência que é algo maravilhoso que auxilia muito em grandes projetos, evitando assim as famosas repetições de código. Acrescenta a camada Service (considere como uma espécie da DAO, já que no flash é muito comum o carregamento de dados externos), tornando assim uma framework MVCS.
É uma framework ideal para projetos de médio e grande porte devido ao seu escopo fortemente limitado e a invervão de controle (IoC).
Neste artigo irei explicar cada componente do PureMVC e num artigo seguinte irei desenvolver uma mini aplicação. Num futuro próximo irei realizar o mesmo procedimento porém com o Robotlegs.
Então, mão na massa!
Meu modelo de projeto
O Flex SDK possui bibliotecas particulares que fazem falta no Flash: o debug do FB (Flash Builder) é sem dúvida muito superior que o do Flash (IDE). É possível fazer projetos no FB interligando com o Flash (IDE), mas minha preferência é 100% Flex.
Todos os assets do projeto eu faço no Flash (IDE), exporto como biblioteca (SWC) e importo no FB. Assim permitindo usar recursos visuais personalizados dentro do Flex.
No Flash, tudo começa com a classe Main que extende MovieClip. Como faremos isto no Flex? Se você fizer um addChild(new MovieClip()) num mxml irá se deparar com uma exception, mas a classe mx.core.UIComponent (<< FlexSprite >>) permite adicionar DisplayObjects dentro dele e no mxml você pode fazer um: this.addElement(new UIComponent())
Veja o exemplo abaixo:
No lugar do MovieClip, você usaria uma classe que extende ele para iniciar toda sua lógica aproveitando assim todos os recursos que o Flex SDK e a IDE podem lhe oferecer.
Porém para este artigo iremos mudar um pouco este exemplo pois pode haver mudança no tamanho da stage o mxml que irá disparar os eventos e não o seus mc internos. Explicarei isto mais adiante.
Façade
A Façade serve para inicializar os core actors (Model, View e Controller). Através dela você pode acessar qualquer core actor registrado à ela.
A imagem abaixo exemplifica muito bem seu funcionamento:
Considere que seu projeto seja um componente, a Façade simplesmente será o seu núcleo. Existe o PureMVC Multicore que basicamente é um conjunto adicional de bibliotecas que permitem a comunicação de um componente com outro.
Criando sua primeira Façade:
- Faça o download da última versão do PureMVC no link http://trac.puremvc.org/PureMVC_AS3/wiki/Downloads e extraia os arquivos do zip para uma pasta qualquer.
- Crie um novo projeto Flex com o nome: Hello_PureMVC e importe a biblioteca do PureMVC.
(Uma forma fácil de imporar a biblioteca é colar o swc na pasta libs do seu projeto) - Em oop é muito importante seguir a convenção de padronização de projetos portanto crie os pacotes para:
Armazenar as classes principais: br.com.[seu_nome]
as classes da model: br.com.[seu_nome].model
os Value Objects : br.com.[seu_nome].model.vo
os Mediators: br.com.[seu_nome].mediator
as Views: br.com.[seu_nome].mediator.ui
e os Controllers: br.com.[seu_nome].controller
(No meu caso ficou br.com.irineu.[…])Sua estrutura ficará semelhante a esta:
- Irei utilizar o meu pacote para todos os exemplo adiantes, portanto no pacote br.com.irineu adicone uma nova classe chamada: ApplicationFacade, que deve extender org.puremvc.as3.patterns.facade.Facade e implementar a interface org.puremvc.as3.interface.Ifacade.
Sua classe ficará parecida com o bloco de código abaixo:
package br.com.irineu.controller { import org.puremvc.as3.interfaces.IFacade; import org.puremvc.as3.patterns.facade.Facade; public class AppplicationFacade extends Facade implements IFacade { public function AppplicationFacade() { super(); } } }
Agora vamos sobrescrever o método: initializeController
Se você estiver usando o FB vá no menu: Source > override/implement methods… – selecione o initializeController():void e dê OK, automaticamente ele irá acrescentar o bloco de código:
override protected function initializeController():void { // TODO Auto Generated method stub super.initializeController(); }
Caso você não esteja usando o FB, apenas copie e cole logo abaixo do construtor da classe.
Até agora tudo tranquilo certo? Criamos um projeto com seus pacotes principais, em seguida criamos uma classe que extende a Facade e implementa a IFacade e finalizamos com uma sobrescrita de método.
Agora crie um método void de acesso público chamado startup que receberá como parâmetro o objeto: Hello_PureMVC.
Ficará parecido com o bloco de código baixo:
public function startup(app:Hello_PureMVC):void { // TODO inserir um código divertido aqui }
Este método que irá disparar a primeira notificação da nossa aplicação, iremos implementar mais adiente.
Notification
O objeto Notification é o recurso de mensageria da framework PureMVC. Sua lógica lembra muito o despacho de eventos, porém muito mais simplificado.
O lado negativo da Notification é que todos os objetos que forem “anexados” serão do tipo Object, portanto sempre será necessário fazer o casting. Você pode extender uma Notification e alterar (ou adicionar) um campo para armazenar seu objeto de tipo mais espefíco, mas no final tudo será reconhecido como a interface INotification, sendo necessário um cast de qualquer maneira para acessar atributos da sua notification específica.
Uma Notification serve para disparar a execução de um Command (falarei sobre Commands mais adiante) que é a melhor prática, porém é possível fazer os mediators também escutarem as Notifications que devido ao “fator preguiça” acaba sendo mais utilizado do que os Commands. Explicarei todos os casos de uso de Notifications apontanto os problemas de casa uso.
O mais legal é que geralmente nós nem instanciamos uma Notification, ela acaba sendo transparente para nós! Command, Façade, Mediator e Proxy possuem um método na sua superclasse:
sendNotification(notificationName:String, body:Object=null, type:String=null); //usamos muito assim: sendNotification(ALGUMA_CONSTANTE, “Hello World!”);
O parâmetro notificationName, para melhor organização, usa-se uma constante estática e final declarada na Façade (lembra muito os caso “despachos de eventos” não é?), que será associado para execução de um command (lembra muito quando usamos uma constante do evento para executar um método não acha?) ou de um interesse de algum mediator (falarei sobre isto mais adiante).
Commands
Um command faz parte da camada controller, no PureMVC existem dois tipos: SimpleCommand e MacroCommand:
SimpleCommand
É apenas uma classe para executar um determinado command disparado por uma Notification.
Atenção!
Não utilize o construtor de um Command! O PureMVC executa apenas o método execute(notification:INotification)
override public function execute(notification:INotification):void { // TODO Auto Generated method stub super.execute(notification); }
No método execute você pode chamar qualquer outro método que você criar, o importante é entender que tudo deve começar no execute.
O parâmetro notification é um objeto notification que possui os atributos anexados no respectivo: sendNotification(?,?,?) que foi enviado.
Para registrar um command é muito simples. Crie uma classe chamada: IniciaAlgoCommand que extenda org.puremvc.as3.patterns.command.SimpleCommand e implemente org.puremvc.as3.interfaces.ICommand e na sua façade insira a linha de código:
facade.registerCommand(ApplicationFacade.INICIA_ALGO, IniciaAlgoCommand);
Desta forma quando se enviar a notification:
sendNotification(ApplicationFacade.INICIA_ALGO,”Hello World”)
Automaticamente será instanciado o command (IniciaAlgoCommand) e será chamado o método execute passando como parâmetro as informações da notification.
MacroCommand
O MacroCommand é um Command com mais funcionalidades: é capaz de adicionar subCommands passando implicitamente a sua notification para os mesmos. É como se fosse o encarregado de mandar executar uma série de commands entende?
Imagine o cenário:
Quando clicar no botão deve carregar um arquivo e também mudar de tela. Você terá que associar uma constante de notification para 2 commands:
facade.registerCommand(ApplicationFacade.INICIA_ALGO, IniciaAlgoCommand); facade.registerCommand(ApplicationFacade.INICIA_ALGO, MudaTelaCommand);
Com o MacroCommand tudo se resume a:
facade.registerCommand(ApplicationFacade.INICIA_ALGO, MeuMacroCommand);
E dentro do MacroCommand:
addSubCommand(IniciaAlgoCommand); addSubCommand(MudaTelaCommand);
De novidade nós temos o método:
- initializeMacroCommand() – que pode ser sobrescrito para declarar os subcommands. Ele, assim como o execute, são chamados pela própria framework;
- addSubCommand(ICommand) – que serve para adicionar subcommands
Por via das boas práticas recomendo usar o MacroCommand principalmente e apenas para adicionar subCommands. Você pode usar para realizar alguma outra tarefa, a escolha é livre, mas não é muito adequado.
Mão na massa!
override public function execute(notification:INotification):void { trace(notification.getBody()); }
Crie uma nova classe seguindo os padrões da anterior mas com o nome: FazAlgoCommand e sobrescreva o método execute:
override public function execute(notification:INotification):void { trace("Vamos criar um código divertido aqui"); }
override protected function initializeMacroCommand():void { addSubCommand(HelloWorldCommand); addSubCommand(FazAlgoCommand); }
No construtor da classe ApplicationFacade registre o command StartupCommand relecionando-o à constante STARTUP e o command TesteCommand à constante TESTE.
public function AppplicationFacade() { registerCommand(STARTUP,StartupCommand); registerCommand(TESTE,TesteCommand); }
No método startup da classe ApplicationFacade envie as notifications e teste a aplicação.
public function startup(app:Hello_PureMVC):void{ // TODO inserir um código divertido aqui sendNotification(STARTUP,"Hello World"); sendNotification(TESTE,"Teste 123"); sendNotification(TESTE,"321Teste"); }
No mxml Hello_PureMVC crie um método void chamado startApp que deve iniciar a ApplicationFacade e passe como parâmetro a instância do mxml, por fim mande executar-lo logo que a aplicação iniciar:
<?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600" applicationComplete="startApp()"> <fx:Declarations> <!-- Place non-visual elements (e.g., services, value objects) here --> </fx:Declarations> <fx:Script> <![CDATA[ import br.com.irineu.ApplicationFacade; private function startApp():void{ new ApplicationFacade().startup(this); } ]]> </fx:Script> </s:Application>
Execute e teste a aplicação.
No final haverá esta saída:
Hello World
Vamos criar um código divertido aqui
Este é o TesteCommand! Teste 123
Este é o TesteCommand! 321Teste
O que foi feito:
Criamos os commands, registramos e disparamos notificações com suas respectivas constantes registradas.
Para a saía “HelloWorld” e “Vamos criar um código divertido aqui”, usamos dois SimpleCommands que foram adicionados no MacroCommand na respectiva ordem da saída. E as frases “Este é o TestCommand! Teste 123” e “Este é o TestCommand! 321Teste”, foram o resultado de duas notificações bom bodys diferentes que foram notificados logo em seguida.
Proxy
“O Proxy faz parte da camada Model. Serve para se manipular dados da model, comunicar com serviços remotos e se necessário persistir.”
Ou seja, suponhamos que você irá carregar um XML , então você deve ter um proxy para fazer o carregamento e outro para a serialização e fornecimento dos Value Objects (VO). Isto lembra muito o modelo MVCS do Robotlegs onde a camada service serve apenas para fazer estes carregamentos e requisições externas.
Atenção!
Um proxy PODE enviar notificações mas NÃO PODE ESCUTÁ-LAS. Para comandar um proxy você deve chamar um método ou propriedade de forma direta.
Assim como os commads um proxy deve ser registrado, porém não se relaciona um proxy à uma constante e sim à um nome passado no construtor.
Como se usa um construtor neste caso, devemos registrar uma instância e não uma classe. Veja no exemplo abaixo:
registerProxy(new MeuProxy(getQualifiedClassName(MeuProxy),"Algum obj")); // public function MeuProxy(proxyName:String=null, data:Object=null)
Você pode registrar um Proxy sem passar seu nome nem objeto e internamente no proxy enviar para a superclasse estas informações. É possível enviar nada para a superclasse mas ai fica a seu critério viver arriscadamente.
Dica:
Até agora, apenas registramos os core actors na façade mas é possível registrar dentro de qualquer outro core actor pois todos possuem uma variável facade que possui o valor de instância da ApplicationFacade! Desta forma em qualquer core actor você está livre para fazer:
facade. registerProxy(new MeuProxy(getQualifiedClassName(MeuProxy),"Algum obj"));
Para recuperar um proxy da facade (por exemplo um proxy), é muito simples:
facade.retrieveProxy(“NomeDoProxy”);
Um exemplo prático:
var proxy:MeuProxy = facade.retrieveProxy(getQualifiedClassName(MeuProxy)) as MeuProxy;
Atenção!
No caso do proxy é possível iniciar sua lógica pelo construtor, porém de qualquer forma a framework irá chamar o método onRegister (semelhante ao caso do Command). É importante entender que o construtor irá ser executado logo que a classe for instanciada e o onRegister quando a framework estiver com a classe pronta para executar. Portanto o ideal é começar diretamente pelo onRegister para evitar possíveis enxaquecas.
Mão na massa!
Crie uma classe no pacote br.com.irineu.model com o nome MeuProxy, que deve extender org.puremvc.as3.patterns.proxy.Proxy e implementar org.puremvc.as3.interfaces.Iproxy
Atenção:
No FlashBuilder ele automaticamente segue os padrões do construtor da superclasse e envia os parâmetros da subclasse para a superclasse. Caso você esteja fazendo manualmente o construtor deve seguir o exemplo abaixo:
public function MeuProxy(proxyName:String=null, data:Object=null) { super(proxyName, data); }
Sobrescreva o método onRegister:
override public function onRegister():void { trace("proxy registrado: " + getProxyName()); trace("url para carregar o xml: " + getData()); }
Crie um método void sem parâmetros com o nome que irá disparar uma Notification da constante TESTE da ApplicationFacade:
public function getNome():String{ return “Algum nome do xml: ”+ getData(); }
No construtor da classe ApplicationFacade registre o proxy:
public function ApplicationFacade() { registerCommand(STARTUP,StartupCommand); registerCommand(TESTE,TesteCommand); registerProxy(new MeuProxy(getQualifiedClassName(MeuProxy),"xml/algumxml.xml")); }
Na classe ApplicationFacade no método startup reova uma duas Notifications que estão relacionadas à constante TESTE:
public function startup(app:Hello_PureMVC):void{ sendNotification(STARTUP,"Hello World"); sendNotification(TESTE,"Teste proxy"); }
Na classe que está relacionada à constante TESTE (TesteCommand), altere o método execute para:
override public function execute(notification:INotification):void { var proxy:MeuProxy = facade.retrieveProxy(getQualifiedClassName(MeuProxy)) as MeuProxy; trace(proxy.getNome()); }
No final haverá esta saída:
proxy registrado: br.com.irineu.model::MeuProxy
url para carregar o xml: xml/algumxml.xml
Hello World
Vamos criar um código divertido aqui
Algum nome do xml: xml/algumxml.xml
O que foi feito:
Criamos, registramos o proxy e criamos um método de acesso público para um command pegar uma string deste mesmo proxy com as informações de quem ele foi registrado (no caso a url e o nome).
Mediator
O mediator será o core actor que certamente será mais implementado pois é a ponte entre sua view e os core actors. Tudo que sua view fizer e for necessário acessar algum outro core actor (no o caso inverso também) ele usará o mediator como intermediário.
A View
Vamos utilizar o Flash (IDE) para criar uma interface simples: insira um texto dinâmico (com nome de instância: tfStatus, não esqueça de embedar as fontes) e 2 botões (bt1 e bt2).
Selecione tudo no palco e transforme num MovieClip pronto para ser exportado:
Em seguida em publish settings ative a opção para exportar o swc (lib).
Por fim salve seu arquivo e exporte o filme. Verifique que no diretório onde se encontra seu .fla surgiram 2 arquivos: um swf e um swc. Copie o swc para a sua pasta libs no FB:
Feito isto vamos dar vida nesta view: crie no pacote br.com.irineu.mediator.ui a classe ExemploUI que extenda br.com.irineu.asset.ExemploView.
Crie um método de acesso público setStatus que deve colcoar no campo de texto (tfStatus) uma mensagem vinda do parâmetro.
O Mediator
Crie uma classe no pacote br.com.irineu.mediator a classe ExemploMediator que extenda org.puremvc.as3.patterns.mediator.Mediator e implemente org.puremvc.as3.interfaces.Imediator.
O Construtor do mediator deverá ser parecido com isto:
public function ExemploMediator(mediatorName:String=null, viewComponent:Object=null) { super(mediatorName, viewComponent); }
Fica à gosto do freguês tipar a view no parâmetro, não receber parâmetro e inserir diretamente no super uma nova view, definir um único nome para o mediator, etc…
No meu caso, gosto de permitir que eu envie um nome, mas que deixe fechado para receber um único ViewComponent:
public function ExemploMediator(mediatorName:String="ExemploMediator") { super(mediatorName, new ExemploUI()); }
Eu havia até feito um macro que gerava este construtor automaticamente
No mediator há algumas semelhanças com o proxy, como os métodos onRegister e sendNotification, até para registrá-lo é parecido com os proxys registerMediator(new ExemploMediator());
Mas tem suas particularidades: pode ouvir Notifications. Como assim? Simples: No Command nós associamos uma notification à eles, já no Mediator nós temos uma Array daquelas constantes que definimos na Façade para disparar Notifications, onde todas as que estiverem contraídas nesta Array irá executar o método handleNotification(notification:INotification).
Esta Array na verdade é um método que sobreescrevemos que retorna uma array:
Uma outra coisa que costumo muito fazer é criar a propriedade (apenas get) para retornar a view:
public function get view():ExemploUI { return super.getViewComponent() as ExemploUI; }
Desta forma fica muito mais fácil acessar métodos e propriedades na nossa view. Sobrescreva o método onRegister deste mediator e adicione eventos de escuta de click para o bt1 e bt2. No bt1 ainda vamos implementar, mas no bt2 apenas limpe o campo da view:
override public function onRegister():void { view.bt1.addEventListener(MouseEvent.CLICK,function():void{ });
view.bt2.addEventListener(MouseEvent.CLICK,function():void{ view.tfStatus.text = ""; }); }
Muito bom, mas para implementar a idéia do uso do Mediator > Command > Proxy, agora você sozinho irá realizar as seguintes tarefas:
- Crie as constantes na Façade: REQUEST_STATUS e SHOW_STATUS.
- No trecho de código que escuta o evento click no bt1, mande disparar a Notification REQUEST_STATUS sem corpo.
- Crie um comando e associe-o à esta constante que foi declara e em seguida registre-o na facade.
- Crie um proxy e nele crie um método que dispare a Notification SHOW_STATUS contendo no body yma string concatenada com o tempo atual.
- No Command que você criou faça o registro do proxy com o tratamento para evitar o re-registramento do proxy quando este command for invocado novamente.
- No ExemploMediator adicione a escuta para uma Notification com nome: SHOW_STATUS e no handle notification adicione um caso para o mesmo, que deve adicionar no campo de texto da View o body da notification.
Se você debugar a aplicação nada irá acontecer porque não inserimos esta view no aplicativo. Apenas criamos um mediator associado à ele (mas ele se encontra inicializado). No arquivo mxml temos:
new ApplicationFacade().startup(this);
Este código inicia a Façade passando uma instância de si. Já na classe ApplciationFacade bloco:
public function startup(app:Hello_PureMVC):void{ sendNotification(STARTUP,"Hello World"); sendNotification(TESTE,"Teste proxy"); }
Altere para:
public function startup(app:Hello_PureMVC):void{ sendNotification(STARTUP,app); }
Comente a linha addSubCommand(HelloWorldCommand), a classe StartupCommand e para finalizar na classe FazAlgoCommand, altere o método execute para:
override public function execute(notification:INotification):void { /*Como neste exemplo não vamos trabalhar com stage não será criado um mediator para o uicomponent e a instância da aplicativo*/ var uiComp:UIComponent = new UIComponent(); (notification.getBody() as Hello_PureMVC).addElement(uiComp); uiComp.addChild(facade.retrieveMediator("ExemploMediator").getViewComponent() as DisplayObject); }
Teste a aplicação e veja o resultado.
Considerações finais
Meu aplicativo cresceu muito e minha façade está com uma lista gigante de constantes.
Neste caso você deve optar pelo PureMVC Multicore, na figura abaixo exemplifica muito bem isto:
Seu aplicativo será um conjunto de façades. Estes projetos geralmente precisam de um grande estudo e planejamento prévio.
Estou em dúvida em relação ao registro dos core actors.
Os principais (para a inicialização) são registrados na façade (preferência apenas o statup) o resto são nos próprios core actors de acordo com a necessidade para não acabar instanciando muitos objetos desnecessariamente de uma vez só. Mas não esqueça de fazer o devido tratamento para ver se um core actor já fora registrado anteriormente, caso exista esta ocorrência.
É muito importante seguir passo a passo cada etapa, montar seus próprios testes e brincadeiras e ver até onde a framework pode te levar (uma técnica auto-didata muito eficiente e divertida). Somente desta forma será possível compreender o funcionamento do PureMVC da melhor forma.
Daqui em diante não haverá mais fórmulas a serem decoradas nem tutoriais a serem seguidos, tudo vai depender de sua criatividade.
Até o próximo!