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.



;)

Um comentário:

  1. Procurando por Design Pattern, encontrei seu site João, espero que continue postando sobre o assunto, pq é complicado encontrar material em português. Parabéns.
    abrs

    ResponderExcluir