DClick

A evolução do Mock usando o Cairngorm


Twitter!

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
  2. {
  3. public function execute(flexDataTransfer:FlexDataTransferObject):FlexDataTransferObject
  4. {
  5.     var flexDTO:FlexDataTransferObject = new FlexDataTransferObject();
  6.     var usuario:UsuarioDTO =  new UsuarioDTO();
  7.     usuario.nome = flexDataTransfer.listaParametros.PARAM_LOGIN;
  8.     flexDTO.listaParametros.usuarioDTO = usuario;
  9.     return flexDTO;
  10. }
  11.    
  12. }

Compartilhe:

  • RSS
  • Twitter
  • del.icio.us
  • Facebook
  • MySpace
  • LinkedIn
  • Google Bookmarks
Por Ricardo Pettine em 15/February/2008 | Comentar | Trackback


No Translations

5 comentários para “A evolução do Mock usando o Cairngorm”


Parabens pelo artigo e pela iniciativa. Sensacional!


Ricardo. Mais uma vez me desculpe pela poluição no seu post.
Peço por favor que observe depois o post que fiz no meu blog a respeito do que estava tentando comentar aqui. Obrigado antecipadamente.

link: http://teclandoalto.blogspot.com/2008/02/flex-cairngorm-e-desenvolvimento.html


Vicente,

Agradeço seu interesse. Sua idéia é bem similar ao que foi proposto sim. O que você chamou de Factory Data na verdade eu chamo de MOCK. Essencialmente o objetivo é o mesmo, que é validarmos o comportamento de nossa interface sem termos uma dependência de algum serviço do back-end. Muitas vezes o serviço do back-end ainda não está disponível e já podemos adiantar grande parte do front-end. Além de quebrarmos a dependência do serviço back-end também garantimos uma integração com menos problemas.

Outra vantagem do MOCK que não mencionei anteriormente é que podemos utilizar a aplicação para apresentações. Às vezes, queremos apresentar aos usuários a aplicação e podemos gerar uma versão MOCK e conseguimos utilizar a aplicação totalmente Offline (sem serviço). No último projeto que trabalhei isto foi feito várias vezes e foi bastante útil.

Em relação à utilização de dois delegates o problema está no custo de escrevermos estes delegates. Veja a seguir um exemplo de um command com acesso direto ao delegate:

[as]
// Exemplo de um Command utilizando apenas um Delegate
public class BuscarProjetosCommand implements ICommand, IResponder
{

private var modelApplication:ModelApplication = ModelApplication.getInstance();

public function execute(event:CairngormEvent):void
{
var delegate:FichaDelegate = new FichaDelegate(this);
delegate.buscarFichaPorID(FichaEvent(event).idFicha);
}

public function result(data:Object):void
{
modelApplication.ficha = data.result.listaParametros.result as FichaDTO;
}

public function fault(info:Object):void
{
FaultMessageManager.showFaultMessage(info);
}

}
[/as]

Reparei que você faz uma modificação no Command para utilizar o FakeData correto? A minha proposta é que não seja modificado nenhum código para que possamos utilizar o MOCK ou o serviço REAL. Portanto o mesmo Command acima será usado para ambas as situações. Para alternarmos entre a modalidade MOCK ou REAL teremos na classe FactoryServiceLocator uma constante que define para qual modalidade iremos gerar a compilação. Veja:
[as]
public class FactoryServiceLocator
{

//muda o serviço REAL ou MOCK
public static var service:String = MOCK;

public static const REAL:String = “1″;
public static const MOCK:String = “2″;

private static var instance:IServiceLocator;
public static function getInstance():IServiceLocator
{
if (instance == null )
{
if(service == REAL)
{
instance = RealServiceLocator.getInstance(“ApplicationServices”);

}
else if(service == MOCK)
{
instance = MockServiceLocator.getInstance();
}
}
return instance;
}

}
[/as]

Portanto, todo o código utilizado para a chamada do serviço back-end será exatamente o mesmo para o MOCK. E quanto menos código escrevermos para utilizar o MOCK melhor. Por isto a proposta de um MOCK inteligente que gera os DTOs a partir de arquivos XML.

Caso tenha interesse eu posso montar uma mini aplicação exemplo com os conceitos que foram citados. E parabéns pelo seu post! Considero esta iniciativa de utilização de dados fakes bastante eficiente e extremamente útil no processo de desenvolvimento do front-end. Espero que outros desenvolvedores também se interessem por esta prática!

Fique a vontade para fazer novos questionamentos!


Oi Ricardo!

Acho que consegui compreender a idéia. Realmente excelente e bem melhor aplicada. Ficaria agradecido se pudesse mandar essa aplicação de exemplo que você tem para que eu possa somente responder questões de organização do package que me surgiram.

Se possível, por favor envie para macieljr@gmail.com.

As idéias agora são:

- Definir um ServiceLocator para ser utilizado no Delegate, de forma que a partir de um parametro o delegate seja capaz de decidir qual service requisitar desde ServiceLocator.

- Fazer com que o Mock/FakeFactory (vou adotar o convencional nome Mock de agora em diante) trabalhe com XML. Vou amadurecer a idéia no objetivo de ter uma classe genérica que simplesmente receba como parametro a classe da qual se necessita instâncias (os DTOs/VOs) e o numero de instâncias necessárias. Com isso, talvez seja interessante que cada DTO/VO tenha a informação do XML respectivo para ser detectado pelo Mock, ou o próprio Mock assuma o nome do XML respectivo a partir do nome da classe.

Muito obrigado pelas informações! Espero conseguir evoluir e apresentar algo.


[...] conhecimento a partir deste post no blog da DClick e ao escrever este post aqui mesmo no meu blog sobre desenvolvimento [...]

Adicionar comentário

(requerido)
(requerido, não será publicado)