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:
-
public class FichaDTO implements IValueObject
-
{
-
public var idFicha:Number = 0;
-
public var numeroFicha:String;
-
public var indicadorEditavel;
-
public var tipoFicha:String;
-
public var statusFicha:String;
-
}
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:
-
public class FichaData
-
{
-
-
public static function getAll():ArrayCollection
-
{
-
var lista:ArrayCollection;
-
-
var fichaDTO1:FichaDTO = new FichaDTO();
-
fichaDTO1.idFicha = 5;
-
fichaDTO1.numeroFicha = "0000000010";
-
fichaDTO1.indicadorEditavel = true;
-
fichaDTO1.tipoFicha = “A”;
-
fichaDTO1.statusFicha = "Cancelada";
-
-
var fichaDTO2:FichaDTO = new FichaDTO();
-
fichaDTO2.idFicha = 6;
-
fichaDTO2.numeroFicha = "000000002A";
-
fichaDTO2.indicadorEditavel = false;
-
fichaDTO2.tipoFicha = “B”;
-
fichaDTO2.statusFicha = "Ativa";
-
-
var fichaDTO3:FichaDTO = new FichaDTO();
-
fichaDTO3.idFicha = 7;
-
fichaDTO3.numeroFicha = "0000000030";
-
fichaDTO3.indicadorEditavel = false;
-
fichaDTO3.tipoFicha = “C”;
-
fichaDTO3.statusFicha = "Ativa";
-
-
lista = new ArrayCollection([fichaDTO1, fichaDTO2, fichaDTO3]);
-
-
return lista;
-
}
Então, agora vamos para o business delegate Mock. E teríamos os métodos abaixo que simulariam o retorno do business delegate real.
-
public function listarFichaCadastral():void
-
{
-
setResults(FichaData.getAll());
-
}
-
-
public function buscarFichaPorId (idFicha:Number):void
-
{
-
setResults(FichaData.getAll().getItemAt(0) as FichaDTO);
-
}
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 version="1.0" encoding="iso-8859-1"?>
-
<items>
-
<item idFicha='5' numeroFicha='0000000010' indicadorEditavel='true' tipoFicha='A' statusFicha='Cancelada/>
-
<item idFicha='6' numeroFicha='000000002A' indicadorEditavel='false' tipoFicha='B' statusFicha='Ativa/>
-
<item idFicha='7' numeroFicha='0000000030' indicadorEditavel='false' tipoFicha='C' statusFicha='Ativa"/>
-
</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.
-
public class MockService implements IService
-
{
-
-
// Constantes
-
private static const packageData:String = "projeto/_dto/_data/";
-
private static const classPathDTO:String = "projeto._dto";
-
private static const classPathAtividade:String = "projeto._business.service.atividade_mock";
-
private static const latency:int = 800; // milliseconds
-
-
// Atividades com comportamento padrão - Atividade [ClassePesquisa do XML, Retorno]
-
private static const atividades:Object = {
-
LogoutAtividade:["",""]
-
,ListarFichaCadastral:["FichaDTO","ArrayCollection"]
-
,BuscarFichaPorID:["FichaDTO","FichaDTO"]
-
};
-
-
// Atividades específicas - Necessário declarar para que seja reconhecida em run-time
-
LoginAtividade;
-
-
// Declaração de variáveis
-
public var serviceId:String = "";
-
private var responder:IResponder;
-
private var resultEvent:ResultEvent;
-
private var faultEvent:FaultEvent;
-
private var resultFlexDTO:FlexDataTransferObject = new FlexDataTransferObject();
-
private var servico:String;
-
private var flexDTO:FlexDataTransferObject;
-
private var myLoader:URLLoader;
-
private var myXML:XML = new XML();
-
private var classDTO:String;
-
private var timer:Timer;
-
-
/**
-
* Construtor
-
* @param callingCommand
-
* @return
-
*
-
*/
-
public function MockService(callingCommand:IResponder)
-
{
-
responder = callingCommand;
-
}
-
-
/**
-
* Método Execute padrão
-
* @param servico
-
* @param flexDTO
-
*
-
*/
-
public function execute(servico:String, flexDTO:FlexDataTransferObject):void
-
{
-
this.flexDTO = flexDTO;
-
this.servico = servico;
-
executeAtividade();
-
}
-
-
/**
-
* Executa atividade simulando o mesmo comportamento do back-end
-
*
-
*/
-
public function executeAtividade():void
-
{
-
// Atividade padrão com classe de Pesquisa e retorno conhecido
-
if (atividades.hasOwnProperty(servico))
-
{
-
// Verifica se os parâmetros de pesquisa e retorno foram setados
-
if(atividades[servico][0]!=''&&atividades[servico][1]!='')
-
{
-
loadXML(atividades[servico][0]);
-
}
-
else
-
{
-
// Retorna sem fazer nada
-
setResults();
-
}
-
}
-
// Atividade especifica que necessita de uma implementação
-
else
-
{
-
try
-
{
-
var ClassReference:Class = getDefinitionByName(classPathAtividade + "." + servico) as Class;
-
var instance:Object = new ClassReference();
-
resultFlexDTO = instance.execute(flexDTO);
-
// Retorno com sucesso
-
setResults();
-
}
-
catch(e:Error)
-
{
-
// Ocorreu uma falha na definição do Mock
-
var fault:Fault = new Fault("MockException", e.toString());
-
callResponderFault(fault);
-
}
-
}
-
}
-
-
/**
-
* Retorna ao Command que chamou com sucesso
-
*/
-
private function callResponderResult():void
-
{
-
resultEvent = new ResultEvent(ResultEvent.RESULT,false,true,resultFlexDTO);
-
responder.result(resultEvent);
-
}
-
-
/**
-
* Inicia o processo de latency
-
*
-
*/
-
private function setResults():void
-
{
-
CursorManager.setBusyCursor();
-
timer = new Timer(latency);
-
timer.addEventListener(TimerEvent.TIMER,timerEventHandler);
-
timer.start();
-
}
-
-
/**
-
* Handler para a função de TimerEvent
-
* @param event
-
*
-
*/
-
private function timerEventHandler(event:TimerEvent):void
-
{
-
timer.stop();
-
CursorManager.removeBusyCursor();
-
callResponderResult();
-
}
-
-
/**
-
* Retorna ao Command que chamou com falha
-
*/
-
private function callResponderFault(fault:Fault):void
-
{
-
faultEvent = new FaultEvent(FaultEvent.FAULT,false,true,fault);
-
responder.fault(faultEvent);
-
}
-
-
/**
-
* Carrega arquivo XML correspondente a classe de pesquisa
-
* @param dataClass
-
*
-
*/
-
private function loadXML(dataClass:String):void
-
{
-
var XML_URL:String = packageData + dataClass + ".xml";
-
var myXMLURL:URLRequest = new URLRequest(XML_URL);
-
classDTO = dataClass;
-
myLoader = new URLLoader(myXMLURL);
-
myLoader.addEventListener(Event.COMPLETE, loadXMLHandler);
-
}
-
-
/**
-
* Handler do arquivo XML - arquivo carregado
-
* @param event
-
*
-
*/
-
private function loadXMLHandler(event:Event):void
-
{
-
xmlLoaded();
-
}
-
-
/**
-
* Quando o arquivo XML é carregado
-
*
-
*/
-
private function xmlLoaded():void
-
{
-
myXML = XML(myLoader.data);
-
-
// Convert o XML para ArrayCollection
-
var resultArrayCollection:ArrayCollection = convertXML2ArrayCollection(myXML, classDTO);
-
-
// Result
-
var result:Object = resultArrayCollection;
-
-
// Seta as propriedades que foram passadas como parâmetros
-
var properties:Array = getClassProperties(flexDTO.listaParametros);
-
-
// Caso algum parâmetro tenha sido passada realiza um filtro
-
if(properties.length> 0)
-
{
-
result = searchOnArrayCollection(resultArrayCollection, properties);
-
}
-
-
var resultClassDefinition:String = atividades[servico][1];
-
// Caso o tipo definido seja diferente de ArrayCollection verifica se o primeiro
-
// item do ArrayCollection corresponde ao parametro passado
-
if (resultFlexDTO!=null && !(resultClassDefinition == 'ArrayCollection')
-
&& (Util.getClassName(resultFlexDTO[0]) == resultClassDefinition))
-
{
-
result = resultFlexDTO[0];
-
}
-
else if (resultClassDefinition != 'ArrayCollection')
-
{
-
result = null;
-
}
-
-
// Retorno com sucesso
-
resultFlexDTO.listaParametros.result = result;
-
setResults();
-
}
-
-
-
/**
-
* Retorna as propriedades de um objeto no formato Qname
-
* @param instance
-
* @return
-
*
-
*/
-
public function getClassProperties(instance:Object):Array
-
{
-
var resultClassInfo:Object = ObjectUtil.getClassInfo(instance);
-
return resultClassInfo.properties
-
}
-
-
/**
-
* Realiza um filtro baseado no critério do FlexDTO
-
* @param resultList
-
* @return
-
*
-
*/
-
public function searchOnArrayCollection(resultList:ArrayCollection, properties:Array):ArrayCollection
-
{
-
var filteredList:ArrayCollection = new ArrayCollection();
-
var dtoInstance:Object = new Object();
-
-
for each (var property:Object in properties)
-
{
-
// Verifica se o parametro passado é um tipo Primitivo
-
if (ObjectUtil.isSimple(flexDTO.listaParametros[property]))
-
{
-
for each (dtoInstance in resultList)
-
{
-
if (dtoInstance.hasOwnProperty(property)&&dtoInstance[property]==flexDTO.listaParametros[property])
-
{
-
filteredList.addItem(dtoInstance);
-
}
-
}
-
}
-
// Verifica se o parametro passado no FlexDTO representa o mesmo parametro de retorno
-
if (Util.getClassName(flexDTO.listaParametros[property]) == atividades[servico][0] )
-
{
-
var listaParam:Array = getParametrosPreenchidos(flexDTO.listaParametros[property]);
-
for each (dtoInstance in resultList)
-
{
-
for each (var param:Object in listaParam)
-
{
-
if (dtoInstance.hasOwnProperty(param)&&dtoInstance[param]==flexDTO.listaParametros[property][param])
-
{
-
filteredList.addItem(dtoInstance);
-
}
-
}
-
}
-
}
-
}
-
return filteredList;
-
}
-
-
/**
-
* Retorna somente os parâmetros de um objeto que foram preenchidos - usado geralmente em filtros
-
* @param param
-
* @return
-
*
-
*/
-
public function getParametrosPreenchidos(param:Object):Array
-
{
-
var properties:Object = getClassProperties(param);
-
var result:Array = new Array();
-
for each (var property:Object in properties)
-
{
-
// Verifica se o parametro está preenchido
-
if( param[property] && ObjectUtil.isSimple(param[property]) && (param[property]!='' || param[property]>0) )
-
{
-
result.push(property);
-
}
-
}
-
return result;
-
}
-
-
/**
-
* Classe responsável por converter um XML em um ArrayCollection
-
* @param xmlRetorno
-
* @param classDTO
-
* @return
-
*
-
*/
-
public function convertXML2ArrayCollection(xmlRetorno:XML, classDTO:String):ArrayCollection
-
{
-
var xmlStr:String = xmlRetorno.toXMLString();
-
var xmlDoc:XMLDocument = new XMLDocument(xmlStr);
-
var decoder:SimpleXMLDecoder = new SimpleXMLDecoder(true);
-
var resultObj:Object = decoder.decodeXML(xmlDoc);
-
var resultXML:Object = resultObj.items.item;
-
var resultList:ArrayCollection = new ArrayCollection();
-
-
for each (var element:Object in resultXML)
-
{
-
var ClassReference:Class = getDefinitionByName(classPathDTO + "." + classDTO) as Class;
-
var instance:Object = null;
-
if (ClassReference)
-
{
-
instance = new ClassReference();
-
var properties:Object = getClassProperties(instance);
-
for each (var property:Object in properties)
-
{
-
if (element.hasOwnProperty(property))
-
{
-
instance[property] = element[property];
-
}
-
}
-
resultList.addItem(instance);
-
}
-
}
-
return resultList;
-
}
-
public class LoginAtividade implements IAtividadeMock
