sábado, 27 de fevereiro de 2010

Colaboração de elementos com Mediator

É comum termos elementos na interface de usuário que colaboram entre si, ou que uma ação de um elemento causa uma reação em outro elemento. Para abstrair a complexabilidade dessa colaboração dos elementos em si, podemos utilizar um design pattern chamado Mediator.


Mediator Class


O Mediator, como o nome sugere, irá mediar essa colaboração, encapsulando toda a complexabilidade do comportamento conjunto em um objeto mediator.

Mediator Sequence


Um exemplo dessa colaboração é um sistema de auto-completar onde o usuário digita os termos em um campo de texto e um outro elemento vai recebendo as dicas, assim que o input tem seu estado modificado (evento onchange) ele avisa o seu mediator que fará o trabalho e repassará para o seu outro objeto que seria uma lista com as sugestões:

Utilizando Javascript para coordenar a interface de usuário, teríamos o seguinte:

index.html
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="pt-br" lang="pt-br">
        <head>
                <title>Mediator</title>
 
                <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 
                <link rel="stylesheet" href="public/css/style.css" type="text/css" />
        </head>
        <body onload="main();">
                <form id="fMediator" action="" method="post" onsubmit="return false; void(0);">
                        <fieldset>
                                <label for="iNome">
                                        <span>Nome: </span>
                                        <input type="text" id="iNome" name="iNome" />
                                </label>
                                <ul id="lTips"></ul>
                        </fieldset>
                </form>
                <script type="text/javascript" src="public/js/Ajax.js"></script>
                <script type="text/javascript" src="public/js/AbstractColleague.js"></script>
                <script type="text/javascript" src="public/js/InputColleague.js"></script>
                <script type="text/javascript" src="public/js/ListColleague.js"></script>
                <script type="text/javascript" src="public/js/Mediator.js"></script>
                <script type="text/javascript">
                        function main(){
                                var server = new Mediator( 'test.php' );
                                var input = new InputColleague( document.getElementById( 'iNome' ) );
                                var list = new ListColleague( document.getElementById( 'lTips' ) );
 
                                input.setMediator( server );
                                list.setMediator( server );
 
                                server.add( input );
                                server.add( list );
                        }
 
                        function muda( nome ){
                                document.getElementById( 'iNome' ).value = nome;
                                document.getElementById( 'lTips' ).innerHTML = '';
                        }
                </script>
        </body>
</html>


AbstractColleague.js
/**
 * Interface para implementação de um Colleague
 * @constructor
 */
function AbstractColleague(){}; 
/**
 * Template method para envio de uma mensagem para o mediator
 * @param String message A mensagem que será enviada para o mediator
 */
AbstractColleague.prototype.send = function( message ){
        this.mediator.send( message , this );
}; 
/**
 * Template method para definição do Mediator
 * @param Mediator mediator
 */
AbstractColleague.prototype.setMediator = function( mediator ){
        if ( mediator instanceof Mediator ){
                this.mediator = mediator;
        } else {
                throw 'Opz, precisamos de um Mediator';
        }
}; 
/**
 * Template method para recuperação do Mediator
 * @return Mediator
 */
AbstractColleague.prototype.getMediator = function(){
        return this.mediator;
}; 
/**
 * Recebe a notificação do mediator caso um Colleague tenha seu estado modificado.
 * Precisa ser implementado nos Colleagues
 * @param String message
 */
AbstractColleague.prototype.notify = function( message ){};

InputColleague.js
/**
 * Implementação de do input
 * @param input Objeto do input onde o usuário irá digitar
 * @constructor
 */
function InputColleague( input ){
        var obj = this;
        var $_ = {
                onfocus : function(){
                        this.onkeydown = function( evt ){
                                if ( evt.keyCode >= 60 ){
                                        if ( this.timer ) clearInterval( this.timer );
                                        this.timer = setInterval( function( temp ){
                                                clearInterval( temp.timer );
                                                obj.getMediator().send( input.value , obj );
                                        } , 10 , this );
                                }
                        }
                },
                onblur : function(){
                        delete this.onkeydown;
                }
        };
       
        input.onfocus = $_.onfocus;
        input.onblur = $_.onblur;
       
        this.input = input;
}; 
InputColleague.prototype = new AbstractColleague(); 
/**
 * Recebe a notificação do mediator que seu Colleague teve seu estado modificado
 * @param String message
 */
InputColleague.prototype.notify = function( message ){
        this.input.value = message;
}; 

ListColleague.js
/**
 * Implementação da listagem de sugestões
 * @param input Objeto do UL que receberá as LIs
 * @constructor
 */
function ListColleague( list ){
        this.list = list;
}; 
ListColleague.prototype = new AbstractColleague(); 
/**
 * Recebe a notificação do mediator que seu Colleague teve seu estado modificado
 * @param String message
 */
ListColleague.prototype.notify = function( message ){
        this.list.innerHTML = message;
};

Com todos os Colleagues definidos, vamos definir agora o Mediator para um auto completar:



Mediator.js
/**
 * Mediator
 * @param String url A URL que será usada para recuperar as sugestões do banco de dados
 * @construct
 */
function Mediator( url ){
        this.ajax = new Ajax();
        this.colleagues = new Array();
        this.url = url;
}; 
/**
 * Adiciona um Colleague que será mediado pelo Mediator
 * @param Colleague colleague
 */
Mediator.prototype.add = function( colleague ){
        if ( colleague instanceof AbstractColleague ){
                this.colleagues.push( colleague );
        } else {
                throw 'Opz, precisamos de um Colleague';
        }
}; 
/**
 * Usado pelos Colleagues para notificar uns aos outros sobre suas mudanças de estado
 * @param String message
 * @param Colleague colleague
 */
Mediator.prototype.send = function( message , colleague ){
        if ( colleague instanceof AbstractColleague ){
                var $this = this;
               
                message = [ 'message' , escape( message ) ].join( '=' );
               
                this.ajax.open( 'POST' , this.url , true );
                this.ajax.send( message );
                this.ajax.onreadystatechange = function(){
                        if ( this.readyState == 4 ){
                                for ( var obj in $this.colleagues ){
                                        if ( $this.colleagues[ obj ] !== colleague ){
                                                $this.colleagues[ obj ].notify( this.responseText );
                                        }
                                }
                        }
                }
               
        } else {
                throw 'Opz, precisamos de um Colleague';
        }
};


Para a parte do servidor usaremos um PHP bem simples que apenas receberá a requisição e retornará um conjunto de <li> que representará as sugestões:




test.php
<?php
$headers = getallheaders(); 
if ( isset( $headers[ 'X-Requested-With' ] ) && ( $headers[ 'X-Requested-With' ] == 'XMLHttpRequest' ) ){
        if ( ( $_SERVER[ 'REQUEST_METHOD' ] == 'POST' ) && isset( $_POST[ 'message' ] ) ){
                $message = sprintf( '%s%%' , $_POST[ 'message' ] );
                $pdo = new PDO( 'mysql:host=127.0.0.1;dbname=test' , 'usuario' , 'senha' );
                $stm = $pdo->prepare( 'SELECT `u`.`usuariosNome` FROM `Usuarios` AS `u` WHERE `u`.`usuariosNome` LIKE :nome;' );
                $stm->bindParam( ':nome' , $message );
 
                if ( $stm->execute() ){
                        $i = 0;
 
                        foreach ( $stm->fetchAll( PDO::FETCH_OBJ ) as $row ){
                                printf( '<li><a href="#" onclick="muda(\'%s\');">%s</a></li>' , $row->usuariosNome , $row->usuariosNome );
                        }
                } else {
                        header( sprintf( '%s 500 Internal Server Error' , $_SERVER[ 'SERVER_PROTOCOL' ] ) );
                        var_dump( $stm->errorInfo() );
                }
        } else {
                header( sprintf( '%s 400 Bad Request' , $_SERVER[ 'SERVER_PROTOCOL' ] ) );
        }
} else {
        header( sprintf( '%s 400 Bad Request' , $_SERVER[ 'SERVER_PROTOCOL' ] ) );
}

Para esse exemplo foi utilizado um banco de dados MySQL com uma tabela de usuários com a seguinte estrutura:

CREATE SCHEMA IF NOT EXISTS `test`;
 CREATE TABLE IF NOT EXISTS `test`.`Usuarios` (
 `idUsuarios` MEDIUMINT(8) UNSIGNED NOT NULL AUTO_INCREMENT,
 `usuariosNome` VARCHAR(45) NOT NULL,
 PRIMARY KEY(`idUsuarios`),
 INDEX `usuarios`(`usuariosNome` ASC)
) ENGINE = MyISAM DEFAULT CHARACTER SET = utf8 COLLATE = utf8_general_ci;

E os seguinte dados:
INSERT INTO `test`.`Usuarios` (`idUsuarios`, `usuariosNome`) VALUES (NULL, 'Joao');
INSERT INTO `test`.`Usuarios` (`idUsuarios`, `usuariosNome`) VALUES (NULL, 'Joao Batista');
INSERT INTO `test`.`Usuarios` (`idUsuarios`, `usuariosNome`) VALUES (NULL, 'Joao Batista Neto');
INSERT INTO `test`.`Usuarios` (`idUsuarios`, `usuariosNome`) VALUES (NULL, 'Joao Neto');
INSERT INTO `test`.`Usuarios` (`idUsuarios`, `usuariosNome`) VALUES (NULL, 'Jose');
INSERT INTO `test`.`Usuarios` (`idUsuarios`, `usuariosNome`) VALUES (NULL, 'Juliano');
INSERT INTO `test`.`Usuarios` (`idUsuarios`, `usuariosNome`) VALUES (NULL, 'Julio');


E para deixar a interface de usuário mais amigável a seguinte folha de estilos:

style.css
* {
        margin                  : 0;
        padding                 : 0;
        border                  : none;
        text-decoration : none;
}
 
body, html {
        margin                  : 10px;
        font-family             : Arial, Helvetica;
        font-size               : 14px;
}
 
form input#iNome {
        width                   : 160px;
        border                  : 1px solid #dadada;
        *margin                 : 0px 0px 0px 4px;
}
 
form label span {
        display                 : inline-block;
        width                   : 50px;
        text-align              : right;
}
 
ul#lTips {
        border                  : 1px solid #dadada;
        width                   : 160px;
        max-height              : 100px;
        overflow                : auto;
        margin                  : 0px 0px 0px 54px;
        *height                 : 0px;
}
 
ul#lTips li a {
        display                 : block;
        width                   : 100%;
        line-height             : 24px;
        height                  : 24px;
        color                   : #333333;
}
 
ul#lTips li a:hover {
        color                   : #FF0000;
}


Agora, sempre que o usuário digitar alguma coisa no input text o Mediator será avisado, enviará a requisição ao servidor e recuperará a lista de sugestões, essa lista é repassada ao outro Colleague responsável por montar a lista de sugestões.



;)

sexta-feira, 26 de fevereiro de 2010

Gerando arquivos .INI com Composite Design Pattern e PHP

Analisando um arquivo .ini é possível perceber que se trata de uma composição de comentários e seções onde cada seção possui um ou mais parâmetros e cada parâmetro é composto pelo par nome=valor.

Para criar esse tipo de arquivo é possível utilizar um padrão de projeto chamado composite:






Apesar de termos conhecimento de que o arquivo .ini é de fato uma composição, cada item dessa composição possui regras próprias e, para que não tenhamos que conhecer essas regras em um único objeto deixamos com que cada item da composição faça seu trabalho permitindo que tratemos todos os elementos de uma forma comum.

Para o nosso arquivo .ini os participantes são:

Comentários e valor de um parâmetro -> Leaf -> São elementos finais, que não possuem nós filhos.
Nome de um parâmetro, seção e o próprio arquivo.ini -> Composite -> Possuem um ou mais nós filhos.

Uma implementação simples em PHP ficaria assim:



INIFormat.php
<?php
 /**
 * Define a interface do Component
 */
interface INIFormat extends IteratorAggregate {
        /**
         * Adiciona um nó filho ao componente
         * @param INIFormat $leaf
         */
        public function add( INIFormat $leaf );
       
        /**
         * Grava o componente no arquivo
         * @param IFileObject $fo
         * @param string $eol Delimitador de fim de linha
         */
        public function write( IFileObject $fo , $eol = PHP_EOL );
}


AbstractINIComposite.php
<?php
 /**
 * Define a interface da composição
 * @abstract
 */
abstract class AbstractINIComposite implements INIFormat {
        private $leafs;

        /**
         * Inicializa o objeto da composição
         */
        public function __construct(){
                $this->leafs = new ArrayIterator();
        }

        /**
         * Adiciona um nó filho ao componente
         * @param INIFormat $leaf
         */
        public function add( INIFormat $leaf ){
                $this->leafs[] = $leaf;
        }

        /**
         * Recupera um Iterator com os nós filhos da composição
         * @return ArrayIterator
         */
        public function getIterator(){
                return $this->leafs;
        }
}


AbstractINILeaf.php
<?php
 /**
 * Define a interface para os nós que não possuem filhos
 * @abstract
 */
abstract class AbstractINILeaf implements INIFormat {
        /**
         * @param INIFormat $leaf
         * @throws LogicException Como uma folha não pode ter nós filhos, sempre dispara um LogicException
         */
        public function add( INIFormat $leaf ){
                throw new LogicException( 'Um nó final não pode ter nós filhos.' );
        }

        /**
         * Como uma folha não pode ter nós filhos, sempre retorna um Iterator vazio
         * @return ArrayIterator
         */
        public function getIterator(){
                static $iterator = null;

                if ( $iterator == null ) $iterator = new ArrayIterator();

                return $iterator;
        }
}


INIParameter.php
<?php
/**
 * Parâmetro de uma seção
 */
class INIParameter extends AbstractINIComposite {
        /**
         * @var string
         */
        private $name;

        /**
         * Constroi um novo parâmetro de seção
         * @param string $name O nome do parâmetro
         */
        public function __construct( $name ){
                parent::__construct();
                $this->name =& $name;
        }

        /**
         * Implementa o método write para gravar no arquivo
         * @param IFileObject $fo Objeto que será usado para gravar no arquivo
         * @param string $eol Delimitador de fim de linha
         */
        public function write( IFileObject $fo , $eol = PHP_EOL ){
                $iterator = $this->getIterator();
                $name = $this->name;

                if ( ( $total = $iterator->count() ) >= 1 ){
                        if ( $total > 1 ){
                                $name = sprintf( '%s[]' , $name );
                        }

                        foreach ( $iterator as $leaf ){
                                $fo->write( sprintf( '%s=' , $name ) );
                                $leaf->write( $fo , $eol );
                        }
                } else {
                        throw new LogicException( sprintf( 'O parâmetro %s precisa possuir pelo menos 1 valor.' , $this->name ) );
                }
        }
}


INIValue.php
<?php
/**
 * Valor de um parâmetro
 */
class INIValue extends AbstractINILeaf {
        private $value;

        /**
         * Constroi o valor de um parâmetro
         * @param string $value
         */
        public function __construct( $value ){
                $this->value =& $value;
        }

        /**
         * Grava o valor no arquivo
         * @param IFileObject $fo
         * @param string $eol
         */
        public function write( IFileObject $fo , $eol = PHP_EOL ){
                $fo->write( sprintf( '%s%s' , $this->value , $eol ) );
        }
}


INIComment.php
<?php
/**
 * Comentário de um arquivo ou seção
 */
class INIComment extends AbstractINILeaf {
        /**
         * @var string
         */
        private $comment;

        /**
         * Constroi o comentário
         * @param string $comment
         */
        public function __construct( $comment ){
                $this->comment =& $comment;
        }

        /**
         * Grava o comentário no arquivo
         * @param IFileObject $fo
         * @param string $eol
         */
        public function write( IFileObject $fo , $eol = PHP_EOL ){
                $fo->write( sprintf( '; %s%s' , $this->comment , $eol ) );
        }
}


INISection.php
<?php
/**
 * Seção de parâmetros
 */
class INISection extends AbstractINIComposite {
        /**
         * @var string
         */
        private $name;

        /**
         * Constroi a nova seção
         * @param string $name
         */
        public function __construct( $name ){
                parent::__construct();
                $this->name =& $name;
        }

        /**
         * Grava a seção no arquivo
         * @param IFileObject $fo
         * @param string $eol
         */
        public function write( IFileObject $fo , $eol = PHP_EOL ){
                $fo->write( sprintf( '[%s]%s' , $this->name , $eol ) );

                foreach ( $this as $leaf ){
                        $leaf->write( $fo , $eol );
                }
        }
}


INIFile.php
<?php
/**
 * Arquivo INI
 */
class INIFile extends AbstractINIComposite {
        /**
         * @var string
         */
        private $file;

        /**
         * Constroi o novo arquivo
         * @param string $file
         */
        public function __construct( $file ){
                parent::__construct();
                $this->file =& $file;
        }

        /**
         * Grava o arquivo em disco
         * @param IFileObject $fo
         * @param string $eol
         */
        public function write( IFileObject $fo , $eol = PHP_EOL ){
                $fo->open( $this->file , 'w+' );

                foreach ( $this as $leaf ){
                        $leaf->write( $fo , $eol );
                }

                $fo->close();
        }
}


Para demonstrar o funcionamento, vamos usar um objeto para gravar em disco que na verdade irá apenas exibir o conteúdo do arquivo INI gerado:


IFileObject.php
<?php
interface IFileObject {
        public function close();
        public function open( $name , $method );
        public function write( $content );
}


FileObject.php
<?php
class FileObject implements IFileObject {
        public function close(){
                echo '--- Fechando o arquivo ---' , PHP_EOL;
        }

        public function open( $name , $method ){
                echo '--- Abrindo o arquivo: ' , $name , ' ---' , PHP_EOL;
        }

        public function write( $content ){
                echo $content;
        }
}


Usando isso tudo:
<?php
$ini = new INIFile( 'teste.ini' );
$ini->add( new INIComment( 'Abaixo uma nova seção' ) );

$imasters = new INISection( 'iMasters' );
$imasters->add( new INIComment( 'O parâmetro teste possui vários valores' ) );
$teste = new INIParameter( 'teste' );
$teste->add( new INIValue( 'valor 1' ) );
$teste->add( new INIValue( 'valor 2' ) );
$teste->add( new INIValue( 'valor 3' ) );
$imasters->add( $teste );

$imasters->add( new INIComment( 'O parâmetro outro possui só um valor' ) );
$outro = new INIParameter( 'outro' );
$outro->add( new INIValue( 'apenas um' ) );
$imasters->add( $outro );

$ini->add( $imasters );
$ini->write( new FileObject() );


A saída:
--- Abrindo o arquivo: teste.ini ---
; Abaixo uma nova seção[iMasters]
; O parâmetro teste possui vários valores
teste[]=valor 1
teste[]=valor 2
teste[]=valor 3
; O parâmetro outro possui só um valor
outro=apenas um
--- Fechando o arquivo ---


Agora é só implementar a classe FileObject para que ela faça o que tem que fazer (gravar em disco) que tudo estará funcionando.

;)