mai 15 2009

Interfaces gráficas flutuantes utilizando o padrão Observer e Tweener

O modismo é criar interfaces gráficas para sites em tela cheia - fullscreen. Estou trabalhando em um site que exige essa funcionalidade de modo que os elementos na interface gráfica distribuam-se corretamente pela tela do usuário de modo a adequar-se a resolução do monitor ou o tamanho da janela do navegador que o flash está sendo executado.

É bem tranquilo tratar essas informações para um, dois, três elementos… Mas para n elementos, como fazer? Neste site podem ser criadas inúmeras janelas, além do habitual menu e imagem de fundo. Ainda é necessário uma série de protótipos até a entrega final do produto e modificar o código em diversos pontos, diversas vezes sem dúvida não é uma boa prática.

A solução que encontrei foi utilizar um padrão de projeto chamado de Observer. Com ele um objeto Subject poderá enviar mensagens para muitos objetos Observers criando uma relação fracamente acoplada para os Observers que poderão ser reutilizados de maneira simples neste ou em outros projetos.

A classe principal enviará informações sobre o tamanho da tela utilizada pelo usuário e notificará os Observers toda vez que o tamanho da tela for alterado pelo usuário.

A classe abstrata ASubject deve conter alguns métodos específicos para notificar os possíveis subjects, bem como anexar subjects e remove-los. Ela será a classe pai do nosso Subject Main.


package {

	import com.adobe.utils.ArrayUtil;
	import flash.display.MovieClip;
	import flash.errors.IllegalOperationError;

	public class ASubject extends MovieClip {

		// Lista com IObservers.
		protected var _observers:Array = new Array();

		// Guarda estado do Subject.
		protected var _state:Object;

		public function get state():Object {
			return _state;
		}

		public function ASubject(c:ASubject):void {
			if (c != this) {
				throw new IllegalOperationError("This class must be overriden");
			}
		}

		/**
		 * Adiciona um objeto IObserver
		 * à lista _observers.
		 * @param o:IObserver
		 */
		public function attach(o:IObserver):void {
			_observers.push(o);
		}

		/**
		 * Remove um objeto IObserver
		 * à lista _observers.
		 * @param o:IObserver
		 */
		public function detach(o:IObserver):void {
			ArrayUtil.removeValueFromArray(_observers, o);
		}

		/**
		 * Notifica objeto IObserver presentes
		 * na lista _observers.
		 * @param s:ASubject
		 */
		public function notify(s:ASubject):void {
			for each(var o in _observers) {
				o.update(this);
			}
		}
	}
}

É necessário criar uma interface para Observer para que o Subject se comunique com ele.


package {

	public interface IObserver {

		/**
		 * Realiza alterações no objeto
		 * que implementa esta interface
		 * e está presente na lista _observers.
		 *
		 * @see ASubject
		 *
		 * @param a:ASubject
		 */
		function update(a:ASubject):void;
	}
}

Criamos um IObserver Menu.


package  {

	import caurina.transitions.*;
	import flash.display.MovieClip;
	import flash.display.Shape;

	public class Menu extends MovieClip implements IObserver {

		private var _background:Shape;

		public function Menu():void {
			draw();
		}

		public function update(a:ASubject):void {
			Tweener.addTween(_background, { x: a.state.width - _background.width - 20,
											y: 20,
											time: 1 } );
		}

		private function draw():void {
			_background = new Shape();
			_background.graphics.beginFill(0x000000);
			_background.graphics.drawRect(0, 0, 180, 250);
			_background.graphics.endFill();
			addChild(_background);
		}
	}
}

Agora anexamos o IObserver Menu ao ASubject Main da nossa aplicação. Note que ASubject é filho de MovieClip, necessário - poderia ser Sprite também - para que nossa classe se torne o Document Class do filme flash.


package {
	import flash.events.Event;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;

	public class Main extends ASubject {

		private var menu:Menu;
		private var footer:Footer;

		public function Main():void {
			super(this);

			menu = new Menu();
			addChild(menu);
			// Adiciona objeto.
			attach(menu);

			stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;
			stage.addEventListener(Event.ACTIVATE, activateHandler);
			stage.addEventListener(Event.RESIZE, resizeHandler);
		}

		private function activateHandler(event:Event):void {
			_state = { width: stage.stageWidth, height: stage.stageHeight };
			// Notifica objetos.
			notify(this);
		}

		private function resizeHandler(event:Event):void {
			_state = { width: stage.stageWidth, height: stage.stageHeight };
			// Notifica objetos.
			notify(this);
		}
	}
}

Agora supondo que eu queira adicionar um novo IObject ao meu projeto basta criar uma nova classe que herde a interface de IObject, como por exemplo a classe Footer abaixo.


package  {

	import caurina.transitions.*;
	import flash.display.MovieClip;
	import flash.display.Shape;

	public class Footer extends MovieClip implements IObserver {

		private var _background:Shape;

		public function Footer():void {
			draw();
		}

		public function update(a:ASubject):void {
			Tweener.addTween(_background, { width: a.state.width,
											y: a.state.height -20,
											time: 1} );
		}

		private function draw():void {
			_background = new Shape();
			_background.graphics.beginFill(0xFF0000);
			_background.graphics.drawRect(0, 0, 530, 20);
			_background.graphics.endFill();
			addChild(_background);
		}
	}
}

Agora para integrar o novo objeto ao meu Subject Main basta alterar seu método construtor para:


public function Main():void {
	super(this);

	menu = new Menu();
	addChild(menu);
	// Adiciona objeto.
	attach(menu);

	footer = new Footer();
	addChild(footer);
	// Adiciona objeto.
	attach(footer);

	stage.scaleMode = StageScaleMode.NO_SCALE;
    stage.align = StageAlign.TOP_LEFT;
	stage.addEventListener(Event.ACTIVATE, activateHandler);
	stage.addEventListener(Event.RESIZE, resizeHandler);
}

E sobre o Tweener? Bem, é muito simples de utiliza-lo, existem inúmeros tutorias e uma boa documentação disponível na Internet. Usei ele apenas para deixa-lo mais #cool. Fica aqui como uma dica.

Baixe os arquivos utilizados como exemplo e o resultado final.


mai 13 2009

Adobe E-seminar - Flash CS4

Nesta quinta-feira, dia 14 de maio de 2009, ocorrerá mais um evento online da Adobe Brasil. Será as 10 horas da manhã e terá duração de uma hora. Informações sobre o conteúdo abordado e inscrição gratuita no site do evento.

[+] update 14.05.2009 às 10:58
Para quem perdeu ou quer rever a apresentação, ela será disponibilizada no link www.adobe.com/br/events


mai 1 2009

Factory Method para serialização de dados XML e JSON em AS3

Introdução

Devido a diversidade de padrões para comunicação entre o Flash/Flex com ambientes externos e a necessidade de tornar o sistema compatível com eles de maneira rápida e flexível, apresento neste post uma solução.

Construiremos um aplicativo flexível para receber diversos formatos de dados de maneira assíncrona sem implementar um tratamento de dado para cada formato em cada ponto do código.

Utilizaremos o padrão de projeto Factory Method para a criação do projeto e sua implementação, e como exemplo de padrão de comunicação dois dos mais comuns: o XML e o JSON.

Modelo da Aplicação

“Os Factorys Methods eliminam a necessidade de anexar classes específicas das aplicações no código. O código lida somente com a interface de Product; portanto, ele pode trabalhar com quaisquer classes ConcreteProduct definidas pelo usuário.” [Gamma, 2000]

Em nosso projeto, a interface do produto, chamado de IProduct, será responsável pela compreensão que o resto do sistema terá sobre nosso produto, apenas os métodos descritos nesta interface poderão ser acessados pelas outras classes do sistema.


package {

	import flash.events.IEventDispatcher;

	/**
	 * Interface para produtos produzidos
	 * pela fábrica.
	 */
	public interface IProduct extends IEventDispatcher {

		/**
		 * Retorna referência para
		 * conteúdo carregado e serializado.
		 * @return Object
		 */
		function get content():Object;

		/**
		 * Carrega conteúdo para ser
		 * serializado.
		 * @param url:String
		 */
		function load(url:String):void;

	}
}

Implementando IProduct, nossos produtos - objetos gerados apartir dos dados contidos em formato XML ou JSON - deverão fornecer neste caso particular dois métodos: o primeiro, load, para requisitar os dados externos; o segundo, content, para que seja obtido os dados carregados por essa classe.
Note que não seria necessário o uso do content se enviassemos os dados dentro do evento criado no EventDispatcher, entretanto este envio não pode ser no retorno do método load pois a requisição é assíncrona e não devemos deixar nosso aplicativo em espera.

Exemplo para dados no formato XML:


package {

	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.net.URLLoader;
	import flash.net.URLRequest;

	/**
	 * Classe responsável por carregar e serializar
	 * o documento XML.
	 */
	public class LoadXML extends EventDispatcher implements IProduct {

		private var _content:Object;

		public function get content():Object {
			return _content;
		}

		public function LoadXML():void {
			//todo ...
		}

		/**
		 * Carrega documento XML.
		 * @param url:String
		 */
		public function load(url:String):void {
			var urlLoader = new URLLoader();
			urlLoader.addEventListener(Event.COMPLETE, parser);
			urlLoader.load(new URLRequest(url));
		}

		/**
		 * Executa serialização do XML.
		 * Este método deve ser alterado para um parser
		 * genérico ou específico para determinada
		 * estrutura XML.
		 * @param event:Event
		 */
		private function parser(event:Event):void {

			var xml:XML = new XML(event.target.data);

			_content = new Object();
			_content.menu = new Object();
			_content.menu.id = xml.@id;
			_content.menu.value = xml.@value;
			_content.menu.popup = new Object();
			_content.menu.popup.menuitem = new Array();

			for (var i:uint = 0; i < xml.popup.menuitem.length(); ++i) {
				_content.menu.popup.menuitem[i] = { value:xml.popup.menuitem[i].@value,
											onclick:xml.popup.menuitem[i].@onclick};
			}

			dispatchEvent(new Event("complete"));
		}
	}
}

Todas as nossas fábricas devem seguir um padrão, logo criaremos uma classe abstrata, assim, caso seja necessário implementarmos novas fábricas de maneira ágil. Toda fábrica deve conter pelo menos um método, aquele que cria os produtos, em nosso aplicativo o create. Este método deve receber como parâmetro pelo menos a classe do produto que deverá ser criado.


package {

	import flash.errors.IllegalOperationError;
	import flash.events.EventDispatcher;

	/**
	 * Classe abstrata para representação da fábrica.
	 * @see http://www.andrecaribe.com.br/blog/?p=28
	 */
	public class AFactoryMethod extends EventDispatcher {

		public function AFactoryMethod(c:AFactoryMethod):void {
			if (c != this) {
				throw new IllegalOperationError("This class should be overriden.");
			}
		}

		/**
		 * Detalhes na classe concreta FactoryMethod.
		 * @param type:String;
		 * @param url:String;
		 */
		public function create(type:String, url:String):void {
			throw new IllegalOperationError("This method should be overriden.");
		}
	}
}

Nossa fábrica deverá ser uma classe contreta, chamada de FactoryMethod. Ela irá requisitar novos produtos e enviar os dados deles à classe cliente, no nosso caso a classe Main, descrita na sessão Resultados.


package {

	import IProduct;
	import flash.errors.IllegalOperationError;
	import flash.events.Event;
	import flash.events.EventDispatcher;

	/**
	 * Classe concreta que reresenta a fábrica.
	 */
	public class FactoryMethod extends AFactoryMethod {

		private var _product:Object;

		/**
		 * Retorna referência para o produto.
		 * @return Object;
		 */
		public function get product():Object {
			return _product;
		}

		public function FactoryMethod():void {
			super(this);
		}

		/**
		 * Este método é responsável por esconder
		 * os detalhes da criação dos produtos.
		 * Deve ser especificado a ele o produto a
		 * ser criado e neste caso o endereço URL
		 * para obtenção dos dados.
		 * @param type:String
		 * @param url:String
		 */
		public override function create(type:String, url:String):void {
			switch(type) {
				case "xml":
					var _xml:IProduct = new LoadXML();
					_xml.addEventListener(Event.COMPLETE, created);
					_xml.load(url);
					break;
				default:
					throw new IllegalOperationError("Type isn't valid.");
			}
		}

		/**
		 * Este método é responsável pela divulgação
		 * do produto à ao objeto que requisitou a
		 * criação.
		 * Neste caso ele é necessário pois a criação
		 * do produto é realizada de maneira assíncrona.
		 * @param event:Event
		 */
		private function created(event:Event):void {
			_product = event.target.content;
			dispatchEvent(new Event("complete"));
		}
	}
}

Evolução

Para que nosso aplicativo deva suportar um novo formato de dados em sua entrada é necessário apenas a criação de uma nova classe para realizar a sua serialização, classe esta que implementa IProduct.

Exemplo com formato de dados JSON:


package {

	import com.adobe.serialization.json.JSON; // AS3CoreLib
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.net.URLRequest;
	import flash.net.URLLoader;

	/**
	 * Classe responsável por carregar e serializar
	 * o documento JSON.
	 */
	public class LoadJSON extends EventDispatcher implements IProduct {

		private var _content:Object;

		public function get content():Object {
			return _content;
		}

		public function LoadJSON():void {
			//todo ...
		}

		/**
		 * Carrega documento JSON.
		 * @param url:String
		 */
		public function load(url:String):void {
			var urlLoader = new URLLoader();
			urlLoader.addEventListener(Event.COMPLETE, parser);
			urlLoader.load(new URLRequest(url));
		}

		private function parser(event:Event):void {
			_content = JSON.decode(event.target.data);
			dispatchEvent(new Event("complete"));
		}
	}
}

Também é necessário uma pequena alteração na classe FactoryMethod, o método create deverá ficar assim:


public override function create(type:String, url:String):void {
	switch(type) {
		case "xml":
			var _xml:IProduct = new LoadXML();
			_xml.addEventListener(Event.COMPLETE, created);
			_xml.load(url);
			break;
		case "json":
			var _json:IProduct = new LoadJSON();
			_json.addEventListener(Event.COMPLETE, created);
			_json.load(url);
			break;
		default:
			throw new IllegalOperationError("Type isn't valid.");
	}
}

Resultados

O cliente da fábrica FactoryMethod. O importante é que nas outras classes do nosso aplicativo não precisamos mudar sua estrutura ou código, os dados foram encapsulados e devem ser tratados da mesma forma.


package  {

	import flash.events.Event;
	import flash.display.MovieClip;

	/**
	 * Note que a manipulação de dados
	 * permanece da mesma forma, tanto para
	 * dados XML como para dados JSON.
	 */
	public class Main extends MovieClip {
		var loadXML:FactoryMethod;
		var loadJSON:FactoryMethod;

		public function Main():void {
			loadXML = new FactoryMethod();
			loadXML.addEventListener(Event.COMPLETE, createdXML);
			loadXML.create("xml", "menu.xml");

			loadJSON = new FactoryMethod();
			loadJSON.addEventListener(Event.COMPLETE, createdJSON);
			loadJSON.create("json", "menu.txt");
		}

		private function createdXML(e:Event):void {
			trace("-- XML --");
			trace(loadXML.product);
			trace("\tid:", loadXML.product.menu.id);
			trace("\tvalue:", loadXML.product.menu.value);
			trace("\tpopup");
			trace("\t\tmenuitem");
			trace("\t\t\tvalue:", loadXML.product.menu.popup.menuitem[0].value);
			trace("\t\t\tonClick:", loadXML.product.menu.popup.menuitem[0].onclick);
			trace("\t\tmenuitem");
			trace("\t\t\tvalue:", loadXML.product.menu.popup.menuitem[1].value);
			trace("\t\t\tonClick:", loadXML.product.menu.popup.menuitem[1].onclick);
			trace("...");
		}

		private function createdJSON(e:Event):void {
			trace("-- JSON --");
			trace(loadJSON.product);
			trace("\tid:", loadJSON.product.menu.id);
			trace("\tvalue:", loadJSON.product.menu.value);
			trace("\tpopup");
			trace("\t\tmenuitem");
			trace("\t\t\tvalue:", loadJSON.product.menu.popup.menuitem[0].value);
			trace("\t\t\tonClick:", loadJSON.product.menu.popup.menuitem[0].onclick);
			trace("\t\tmenuitem");
			trace("\t\t\tvalue:", loadJSON.product.menu.popup.menuitem[1].value);
			trace("\t\t\tonClick:", loadJSON.product.menu.popup.menuitem[1].onclick);
			trace("...");
		}
	}
}

Resultado no output do Flash.


/* OUTPUT

-- XML --
[object Object]
	id: file
	value: File
	popup
		menuitem
			value: New
			onClick: CreateNewDoc()
		menuitem
			value: Open
			onClick: OpenDoc()
...
-- JSON --
[object Object]
	id: file
	value: File
	popup
		menuitem
			value: New
			onClick: CreateNewDoc()
		menuitem
			value: Open
			onClick: OpenDoc()
...

*/

Conclusão

A evolução é rápida e confiável, de maneira desacoplada ao aplicativo, assim os testes para novas classes de serialização poderão ser realizados de forma independente ao sistema, pois não geram impactos em muitas classes. O único ponto de alteração é a classe FactoryMethod.

Referências

GAMMA, E. HELM, R. JOHNSON, R. VLISSIDES, J. Padrões de Projetos: Soluções reutilizáveis de software orientado a objetos. Trad. Salgado, L. 1. ed. Porto Alegre: Bookman, 2000.

SANDERS, W. CUMARANATUNGE, C. ActionScript 3.0 Design Patterns. 1. ed. O’Reilly Media, 2007.

Exemplos

Baixe aqui o arquivo contendo os exemplos.


abr 13 2009

Otimização de desempenho em código ActionScript

Estou de volta. (:
Continuando com nosso assunto sobre processamento…

Bem, aqui vai uma dica que valem por muitas dicas para otimizar o custo de processamento em programas escritos com ActionScript 2.0 ou 3.0.

Joa Ebert criou uma wiki com informações sobre otimização de código e estruturas de dados para AS. Particularmente tenho maior interese em informações sobre operadores de bits, que apesar de reduzirem a legibilidade do código dão um excelente ganho de desenpenho. Mais dicas sobre operadores de bits em um post bitwise gems fast integer math do Polygonal.

Um dos exemplos do Polygonal:


// Uma simples multiplicação de um número por dois:
x = x * 2;
// Pode ser 300% mais rápido se for escrita assim:
x = x < < 1;
// O mesmo vale para divisões:
x = x / 2;
// Mas o ganho é de 350%:
x = x >> 1;
// E para todas as potências de dois:
x = x / 1024;
// Como por exemplo:
x = x >> 10; 

Não deixem de conferir os links do post! =D


abr 8 2009

Custo de processamento em aplicativos web

Poucas pessoas se preocupam com o ganho de desempenho nos aplicativos, há não ser quando este torna-se crítico para o sucesso do sistema como em um sistema embarcado, jogos que exigem alto processamento de imagens e vetores ou sistemas de computo intensivo.

Preocupar-se com o desempenho de processamento de um site seja em Flash, AJAX, HTML ou o que quer que seja, por exemplo, para alguns pode ser tolice, a maioria se preocupa mais com o tamanho total do produto final, para que seja rápido a visualização do produto na tela do usuário, ou com a beleza que o produto agrega. Geralmente a preocupação está em torno da beleza versus tamanho.

Apesar da memória do computador e da capacidade de processamento parecer sempre superior a de um site esquecemos que nem todos os computadores são como os nossos. Muitos deles estão obsoletos, ainda mais com essa história de inclusão digital - não que eu seja contra - e OLPCs. Lembrando também que não é só o hardware que fica mais rápido, mas os programas e sistemas operacionais também consomem mais.

Partindo agora especificamente para aplicativos Flash é fato que seu desempenho é consideravelmente diferente nos browsers. Então se caro programador testa apenas no Internet Explorer X e no Firefox, sinto muito. O IE x roda muito bem, talvez a única coisa que ele faça de melhor que os outros, o Safari para MAC OSX roda bem, o Firefox fica um pouco atrás, o Chrome fica mais distante, o Opera fica longe e por ai vai…

Tudo bem, a grande maioria utiliza IE x e Firefox, mas será que devemos desprezar o outro público? Talvez um público mais crítico que os demais.

E para mim, o maior fato de que devemos nos preocupar com a carga de processamento é a própria evolução do Flash. É comum utilizar recursos em 3d, filters, interpolações e isso é extremamente custoso. Esses avanços atraem a ganância do arquiteto web inexperiente muitas vezes induzido pelo cliente.

Creio que temos vários pontos a relevar na construção de um site, hotsite ou aplicativo web, quando trata-se de Web temos ainda que abranger um publico muito diversificado logo devemos estabelecer um limite máximo de consumo para nossas contruções que está diretamente associado ao público alvo. Este limite máximo irá corresponder ao desempenho mínimo aceitável e nada de fazer algo que ultrapasse ele, nunca.