DClick

A evolução do Mock usando o Cairngorm


O conceito de Mock foi bastante utilizado nos projetos do Rio de Janeiro. A idéia é bastante interessante para criarmos uma abstração da camada de backend, permitindo que o desenvolvimento Flex possa andar em paralelo e independente desde que o contrato backend/frontend esteja bem definido. Além disto podemos também garantir um processo de integração mais eficiente.

Como funciona?

Normalmente ao dispararmos, por exemplo, um CairngormEvent esperamos que um serviço no backend seja chamado e devolva algum resultado. Quando utilizamos o conceito de mock ao dispararmos este mesmo CairngormEvent não será chamado um serviço no backend, mas sim uma rotina local, que irá retornar o mesmo tipo de dado retornado pelo backend. Desta forma podemos validar alguns comportamentos da interface durante a fase de desenvolvimento e reduzir o tempo de integração com o backend.

Primeira abordagem

Inicialmente foi utilizado o conceito de Factory para o Business Delegate. Então temos dois business delegates: um Mock e outro Real.

Por exemplo :

Vamos imaginar uma busca por fichas cadastrais. E vamos considerar os seguintes CairngormEvents que podem ser disparados:

1)ListarFichaCadastral – Retorna um ArrayCollection com todas as fichas cadastradas
2)BuscarFichaPorId – Retorna apenas uma instancia de ficha para o id fornecido

Considerando a classe DTO abaixo:

Actionscript:
  1. public class FichaDTO implements IValueObject
  2. {
  3.        public var idFicha:Number = 0;
  4.        public var numeroFicha:String;
  5.        public var indicadorEditavel;
  6.        public var tipoFicha:String;
  7.        public var statusFicha:String;
  8. }

O primeiro passo seria construirmos uma classe de dados para retornar possíveis instancias da classe DTO descrita acima. Então teríamos o seguinte:

Actionscript:
  1. public class FichaData
  2. {
  3.  
  4. public static function getAll():ArrayCollection
  5. {
  6.     var lista:ArrayCollection;
  7.    
  8.     var fichaDTO1:FichaDTO = new FichaDTO();
  9.     fichaDTO1.idFicha = 5;
  10.     fichaDTO1.numeroFicha = "0000000010";
  11.     fichaDTO1.indicadorEditavel = true;
  12.     fichaDTO1.tipoFicha = “A”;
  13.     fichaDTO1.statusFicha = "Cancelada";
  14.     
  15.     var fichaDTO2:FichaDTO = new FichaDTO();
  16.     fichaDTO2.idFicha = 6;
  17.     fichaDTO2.numeroFicha = "000000002A";
  18.     fichaDTO2.indicadorEditavel = false;
  19.     fichaDTO2.tipoFicha = “B”;
  20.     fichaDTO2.statusFicha = "Ativa";
  21.    
  22.     var fichaDTO3:FichaDTO = new FichaDTO();
  23.     fichaDTO3.idFicha = 7;
  24.     fichaDTO3.numeroFicha = "0000000030";
  25.     fichaDTO3.indicadorEditavel = false;
  26.     fichaDTO3.tipoFicha = “C”;
  27.     fichaDTO3.statusFicha = "Ativa";
  28.    
  29.     lista = new ArrayCollection([fichaDTO1, fichaDTO2, fichaDTO3]);
  30.  
  31.     return lista;
  32. }

Então, agora vamos para o business delegate Mock. E teríamos os métodos abaixo que simulariam o retorno do business delegate real.

Actionscript:
  1. public function listarFichaCadastral():void
  2. {
  3.     setResults(FichaData.getAll());
  4. }
  5.  
  6. public function buscarFichaPorId (idFicha:Number):void
  7. {
  8.     setResults(FichaData.getAll().getItemAt(0) as FichaDTO);
  9. }

Obs.: A função setResults faria o papel de setar o responder.result e acrescentar um delay, simulando também um atraso no tempo de resposta da chamada.

Qual o grande problema desta abordagem?

Mesmo que possamos reutilizar as classes de dados em qualquer parte do projeto ainda temos um grande esforço para escrevê-las e mantermos atualizadas. Já que sabemos que novos atributos podem surgir nas classes de DTOs ou até mesmo algum refactor possa ocorrer.

Segunda abordagem

Para tentar amenizar o problema descrito acima temos uma segunda proposta.

Agora não iremos mais trabalhar utilizando o conceito de Factory do Business Delegate. Vamos utilizar o conceito de Factory no Service Locator. Então teremos sempre um único delegate. Que por sua vez poderá chamar diferentes serviços, neste caso será um serviço Mock e outro Real. Desta forma, já estamos economizando na escrita dos delegates.

Mas só isto não resolve o problema. Ainda teríamos que escrever o retorno de cada classe de dados. Então, a grande mudança será que a classe de dados não mais precisará ser escrita na grande maioria dos casos.

Mas como?

Agora teremos as classes de dados sendo representadas por arquivos XML. Estes arquivos podem ser facilmente gerados a partir do banco de dados. Então continuando com o exemplo anterior teríamos o seguinte arquivo XML :

XML:
  1. <?xml version="1.0" encoding="iso-8859-1"?>
  2. <items>
  3.     <item idFicha='5' numeroFicha='0000000010' indicadorEditavel='true' tipoFicha='A' statusFicha='Cancelada/>
  4.     <item idFicha='6' numeroFicha='000000002A' indicadorEditavel='false' tipoFicha='B' statusFicha='Ativa/>
  5.     <item idFicha='7' numeroFicha='0000000030' indicadorEditavel='false' tipoFicha='C' statusFicha='Ativa"/>
  6. </items>

E o grande diferencial estará na forma como o serviço é retornado. A classe MockService listada logo abaixo possui uma inteligência que para os casos mais comuns saberá como retornar os dados sem que precise ser escrita nenhuma lógica de retorno.

Ainda assim poderemos escrever, para os casos que o comportamento padrão não atenda as nossas necessidades, uma classe de retorno específica. No exemplo a seguir, o LoginAtividade teria um comportamento específico, retornando por exemplo o próprio login fornecido.

Actionscript:
  1. public class MockService implements IService
  2. {
  3.  
  4. // Constantes
  5. private static const packageData:String = "projeto/_dto/_data/";
  6. private static const classPathDTO:String = "projeto._dto";
  7. private static const classPathAtividade:String = "projeto._business.service.atividade_mock";
  8. private static const latency:int = 800; // milliseconds
  9.  
  10. // Atividades com comportamento padrão - Atividade [ClassePesquisa do XML, Retorno]
  11. private static const atividades:Object = {
  12.      LogoutAtividade:["",""]
  13.     ,ListarFichaCadastral:["FichaDTO","ArrayCollection"]
  14.     ,BuscarFichaPorID:["FichaDTO","FichaDTO"]
  15. };
  16.  
  17. // Atividades específicas - Necessário declarar para que seja reconhecida em run-time
  18. LoginAtividade;
  19.  
  20. // Declaração de variáveis
  21. public var serviceId:String = "";
  22. private var responder:IResponder;
  23. private var resultEvent:ResultEvent;
  24. private var faultEvent:FaultEvent;
  25. private var resultFlexDTO:FlexDataTransferObject = new FlexDataTransferObject();
  26. private var servico:String;
  27. private var flexDTO:FlexDataTransferObject;
  28. private var myLoader:URLLoader;
  29. private var myXML:XML = new XML();
  30. private var classDTO:String;
  31. private var timer:Timer;
  32.  
  33. /**
  34. * Construtor
  35. * @param callingCommand
  36. * @return
  37. *
  38. */
  39. public function MockService(callingCommand:IResponder)
  40. {
  41.     responder = callingCommand;
  42. }
  43.  
  44. /**
  45. * Método Execute padrão
  46. * @param servico
  47. * @param flexDTO
  48. *
  49. */
  50. public function execute(servico:String, flexDTO:FlexDataTransferObject):void
  51. {
  52.     this.flexDTO = flexDTO;
  53.     this.servico = servico;
  54.     executeAtividade();
  55. }
  56.  
  57. /**
  58. * Executa atividade simulando o mesmo comportamento do back-end
  59. *
  60. */
  61. public function executeAtividade():void
  62. {
  63.     // Atividade padrão com classe de Pesquisa e retorno conhecido
  64.     if (atividades.hasOwnProperty(servico))
  65.     {
  66.         // Verifica se os parâmetros de pesquisa e retorno foram setados
  67.         if(atividades[servico][0]!=''&&atividades[servico][1]!='')
  68.         {
  69.             loadXML(atividades[servico][0]);
  70.         }
  71.         else
  72.         {
  73.             // Retorna sem fazer nada
  74.             setResults();
  75.         }
  76.     }
  77.     // Atividade especifica que necessita de uma implementação
  78.     else
  79.     {
  80.         try
  81.         {
  82.             var ClassReference:Class = getDefinitionByName(classPathAtividade + "." + servico) as Class;
  83.             var instance:Object = new ClassReference();
  84.             resultFlexDTO = instance.execute(flexDTO);
  85.             // Retorno com sucesso
  86.             setResults();
  87.         }
  88.         catch(e:Error)
  89.         {
  90.             // Ocorreu uma falha na definição do Mock
  91.             var fault:Fault = new Fault("MockException", e.toString());
  92.             callResponderFault(fault);
  93.         }
  94.     }
  95. }
  96.  
  97. /**
  98. * Retorna ao Command que chamou com sucesso
  99. */
  100. private function callResponderResult():void
  101. {
  102.     resultEvent = new ResultEvent(ResultEvent.RESULT,false,true,resultFlexDTO);
  103.     responder.result(resultEvent);
  104. }
  105.        
  106. /**
  107. * Inicia o processo de latency
  108. *
  109. */
  110. private function setResults():void
  111. {
  112.     CursorManager.setBusyCursor();
  113.     timer = new Timer(latency);
  114.     timer.addEventListener(TimerEvent.TIMER,timerEventHandler);
  115.     timer.start();
  116. }
  117.  
  118. /**
  119. * Handler para a função de TimerEvent
  120. * @param event
  121. *
  122. */
  123. private function timerEventHandler(event:TimerEvent):void
  124. {
  125.     timer.stop();
  126.     CursorManager.removeBusyCursor();
  127.     callResponderResult();
  128. }
  129.  
  130. /**
  131. * Retorna ao Command que chamou com falha
  132. */
  133. private function callResponderFault(fault:Fault):void
  134. {
  135.     faultEvent = new FaultEvent(FaultEvent.FAULT,false,true,fault);
  136.     responder.fault(faultEvent);
  137. }
  138.  
  139. /**
  140. * Carrega arquivo XML correspondente a classe de pesquisa
  141. * @param dataClass
  142. *
  143. */
  144. private function loadXML(dataClass:String):void
  145. {
  146.     var XML_URL:String = packageData + dataClass + ".xml";
  147.     var myXMLURL:URLRequest = new URLRequest(XML_URL);
  148.     classDTO = dataClass;
  149.     myLoader = new URLLoader(myXMLURL);
  150.     myLoader.addEventListener(Event.COMPLETE, loadXMLHandler);
  151. }
  152.  
  153. /**
  154. * Handler do arquivo XML - arquivo carregado
  155. * @param event
  156. *
  157. */
  158. private function loadXMLHandler(event:Event):void
  159. {
  160.     xmlLoaded();
  161. }
  162.  
  163. /**
  164. * Quando o arquivo XML é carregado
  165. *
  166. */
  167. private function xmlLoaded():void
  168. {
  169.     myXML = XML(myLoader.data);
  170.     
  171.     // Convert o XML para ArrayCollection
  172.     var resultArrayCollection:ArrayCollection = convertXML2ArrayCollection(myXML, classDTO);
  173.  
  174.     // Result
  175.     var result:Object = resultArrayCollection;
  176.    
  177.     // Seta as propriedades que foram passadas como parâmetros
  178.     var properties:Array = getClassProperties(flexDTO.listaParametros);
  179.  
  180.     // Caso algum parâmetro tenha sido passada realiza um filtro
  181.     if(properties.length> 0)
  182.     {
  183.         result = searchOnArrayCollection(resultArrayCollection, properties);
  184.     }
  185.    
  186.     var resultClassDefinition:String = atividades[servico][1];
  187.     // Caso o tipo definido seja diferente de ArrayCollection verifica se o primeiro
  188.     // item do ArrayCollection corresponde ao parametro passado
  189.     if (resultFlexDTO!=null && !(resultClassDefinition == 'ArrayCollection')
  190.     && (Util.getClassName(resultFlexDTO[0]) == resultClassDefinition))
  191.     {
  192.         result = resultFlexDTO[0];
  193.     }
  194.     else if (resultClassDefinition != 'ArrayCollection')
  195.     {
  196.         result = null;
  197.     }
  198.    
  199.     // Retorno com sucesso
  200.     resultFlexDTO.listaParametros.result = result;
  201.     setResults();
  202. }
  203.  
  204.  
  205. /**
  206. * Retorna as propriedades de um objeto no formato Qname
  207. * @param instance
  208. * @return
  209. *
  210. */
  211. public function getClassProperties(instance:Object):Array
  212. {
  213.     var resultClassInfo:Object = ObjectUtil.getClassInfo(instance);
  214.     return resultClassInfo.properties
  215. }
  216.  
  217. /**
  218. * Realiza um filtro baseado no critério do FlexDTO
  219. * @param resultList
  220. * @return
  221. *
  222. */
  223. public function searchOnArrayCollection(resultList:ArrayCollection, properties:Array):ArrayCollection
  224. {
  225.     var filteredList:ArrayCollection = new ArrayCollection();
  226.     var dtoInstance:Object = new Object();
  227.     
  228.     for each (var property:Object in properties)
  229.     {
  230.         // Verifica se o parametro passado é um tipo Primitivo
  231.         if (ObjectUtil.isSimple(flexDTO.listaParametros[property]))
  232.         {
  233.             for each (dtoInstance in resultList)
  234.             {
  235.                 if (dtoInstance.hasOwnProperty(property)&&dtoInstance[property]==flexDTO.listaParametros[property])
  236.                 {
  237.                     filteredList.addItem(dtoInstance);
  238.                 }
  239.             }
  240.         }
  241.         // Verifica se o parametro passado no FlexDTO representa o mesmo parametro de retorno
  242.         if (Util.getClassName(flexDTO.listaParametros[property]) == atividades[servico][0] )
  243.         {
  244.             var listaParam:Array = getParametrosPreenchidos(flexDTO.listaParametros[property]);
  245.             for each (dtoInstance in resultList)
  246.             {
  247.                 for each (var param:Object in listaParam)
  248.                 {
  249.                     if (dtoInstance.hasOwnProperty(param)&&dtoInstance[param]==flexDTO.listaParametros[property][param])
  250.                     {
  251.                         filteredList.addItem(dtoInstance);
  252.                     }
  253.                 }
  254.             }
  255.         }
  256.     }
  257.     return filteredList;
  258. }
  259.  
  260. /**
  261. * Retorna somente os parâmetros de um objeto que foram preenchidos - usado geralmente em filtros
  262. * @param param
  263. * @return
  264. *
  265. */
  266. public function getParametrosPreenchidos(param:Object):Array
  267. {
  268.     var properties:Object = getClassProperties(param);
  269.     var result:Array = new Array();
  270.     for each (var property:Object in properties)
  271.     {
  272.         // Verifica se o parametro está preenchido
  273.         if( param[property] && ObjectUtil.isSimple(param[property]) && (param[property]!='' || param[property]>0) )
  274.         {
  275.             result.push(property);
  276.         }
  277.     }
  278.     return result;
  279. }
  280.  
  281. /**
  282. * Classe responsável por converter um XML em um ArrayCollection
  283. * @param xmlRetorno
  284. * @param classDTO
  285. * @return
  286. *
  287. */ 
  288. public function convertXML2ArrayCollection(xmlRetorno:XML, classDTO:String):ArrayCollection
  289. {
  290.     var xmlStr:String = xmlRetorno.toXMLString();
  291.     var xmlDoc:XMLDocument = new XMLDocument(xmlStr);
  292.     var decoder:SimpleXMLDecoder = new SimpleXMLDecoder(true);
  293.     var resultObj:Object = decoder.decodeXML(xmlDoc);
  294.     var resultXML:Object = resultObj.items.item;
  295.     var resultList:ArrayCollection = new ArrayCollection();
  296.  
  297.     for each (var element:Object in resultXML)
  298.     {
  299.         var ClassReference:Class = getDefinitionByName(classPathDTO + "." + classDTO) as Class;
  300.         var instance:Object = null;
  301.         if (ClassReference)
  302.         {
  303.             instance = new ClassReference();
  304.             var properties:Object = getClassProperties(instance);
  305.             for each (var property:Object in properties)
  306.             {
  307.                 if (element.hasOwnProperty(property))
  308.                 {
  309.                     instance[property] = element[property];
  310.                 }
  311.             }
  312.             resultList.addItem(instance);
  313.         }
  314.     }
  315.     return resultList;
  316. }

Actionscript:
  1. public class LoginAtividade implements IAtividadeMock