sábado, dezembro 02, 2006

Projeto de Interfaces : A Relevância em Contexto

Hoje, iniciei um novo projeto. Nas apresentações iniciais para formação da equipe de trabalho, eu e um dos analistas discutíamos ferrenhamente quanto ao uso do Hibernate em aplicações OO: eu favorável, ele contrário.
Seu argumento era que o Hibernate trazia dados desnecessários na grande maioria das vezes, além de não ter a capacidade de fazer operações rowset. Deu um exemplo de cadastro (eu adoro esses exemplos!). Deixei ele falar:

"Nos sistemas OO, geralmente uma tela de cadastro tem relação 1-1 com um objeto de dado (javabean, por exemplo). Assim, quando esse objeto for muito grande e tiver muitas dependências, seu carregamento é demorado, mesmo sendo um objeto só. Veja por exemplo um objetão (palavras dele) com vários relacionamentos n-1 que geram combos. Suponha que essa foreignkey aponte para uma tabela com 1 milhão de registros (sempre usam esse número). Quando o cadastro for exibido, só essa combo demorará vários segundos para ser preenchida. Isso sem contar as outras dependências, que podem ser 1-n e até n-n. Eu prefiro SQL, é muito mais rápido. [E continuou, enquanto seu café esfriava...]

Argumentei várias coisas em favor do Hibernate, como consultas nomeadas, mapeamento explícito, códigos personalizados e, claro, SQL nativo em certos casos. Ele franziu a testa e continuava duvidando, dizendo que seu histórico com tudo isso não era nada agradável...

Foi então que me ocorreu de falar que, de fato, o problema que ele tanto comentava não era o Hibernate, mas sim algo muito anterior a ele. O problema estava no projeto da interface do usuário. Ele fez uma cara estranha, os outros dois analistas puxaram suas cadeiras mais para perto e ele tomou aquele café gelado num gole só. Talvez louco para saber a besteira que eu estava prestes a falar...

Interfaces lentas: a origem do problema
Era sim um problema de interface de usuário, disse eu. Você pode ter um (e na maioria das vezes terá) um objeto associado a uma tela de cadastro, isso até facilita a implementação de patterns, como o Memento e outras coisas, como relações um-para-um entre propriedades e controles, etc. E isso não implica necessariamente em degradação de performance usando o Hibernate. O que você precisa é construir uma interface baseada em contexto de uso, tendo precisa noção da carga de dados realmente necessária para essa interface [gostei da cara dele e dos analistas quando falei isso]. Ele pediu 2 minutos para buscar outro café enquanto um dos analistas encheu um chimarrão nesse meio tempo.

Tentei dar um exemplo simples, e esse mesmo exemplo vou mostrar aqui.

Relevância em contexto
Imagine uma tela bem simples, com uma caixa de seleção (combobox) apenas. Nessa tela, o objetivo do usuário é escolher um entregador de mercadorias e, a partir dessa seleção saber quais são os seus pontos de entrega. Esses pontos de entrega são mostrados em uma listagem, que está logo abaixo, como na figura ao lado:
Aqui, eu disse a ele, está um exemplo do que tu falou: Uma tela que pode ser extremamente lenta, caso muitos registros de entregadores existam (por exemplo, uma empresa internacional que mostre todos os entregadores cadastrados no mundo todo - desculpem, mas foi isso que me veio à mente na hora). Quando essa tela for chamada, provavelmente ela vai demorar alguns bons segundos para carregar. Ele concordou avidamente.
Disse a ele: Aqui está um erro de interface, porque você está carregando muitos registros na combo que, mesmo ordenados, vão ser de difícil manuseio, porque a barra de rolagem fica muito reduzida (item 1 na figura) e, principalmente, porque a maioria dos registros será inútil. Inútil porque o usuário vai escolher somente um deles. Assim, se você tiver 1 milhão de registros, a taxa de acerto da interface para esse controle é de 0,0001% (1 interação de usuário x 1 registro escolhido x 1 milhão de registros na listagem). Continuei.
Como vê, não é o Hibernate que está errado, mas o programador que fez a tela e, antes que ele, o projetista que não especificou como essa caixa de seleção deveria ser carregada.

Melhorando o desempenho
Seria de bom grado se nessa caixa de seleção estivessem presente somentes os registros com maiores possibilidades de serem escolhidos pelo usuário. Ele concordou (os analistas também). Nesse sentido, como no item 1 da figura 2, poderiam ser exibidos na caixa de seleção somente os entregadores vinculados à filial onde o sistema está instalado (uma regra de negócio). Também poderia ser provido um valor padrão (com combo carregada durante sua expansão ao clique do usuário - e mesmo em uma thread auxiliar). Outra alternativa ainda seria o analista ter definido um valor padrão inicial fixo junto ao usuário - item 2 da figura 2. Tudo isso são coisas que os analistas poderiam identificar e propor antecipadamente. São opções inicais boas e simples, disse eu, que melhorariam a taxa de acerto para algo entre 0,2% (1 para ~500 itens na combo) e 15% (considerando um elemento padrão com 10% de corerência e sendo generoso ao admitir que o usuário pode digitar 1 caractere inicial e ter uma faixa de 20 elementos para pesquisar - claro, considerando uma distribuição uniforme dos 500 nomes nas 26 letras do alfabeto ocidental) - uma melhora de 15.000% na taxa de acerto e uma redução de carga de 2000x no BD. Nesse momento, ganhei o público, mas os analistas não gostaram muito de saber que eles poderiam ser culpados da demora na tela (...)

Melhorando um pouco mais o desempenho
Como eu tinha conquistado a atenção, continuei. A interface poderia ser melhorada caso fosse colocado em evidência o contexto do usuário. Além do valor padrão e da exibição dos entregadores vinculados à filial, poderiam ser os elementos da seleção restringidos pelo perfil do usuário em conjunto com seu contexto de uso. Restringir pelo perfil do usuário é simples, marcando os registros selecionados ao longo do tempo e armazenando-os em um cache de segundo ou terceiro níveis (item 3, figura 3). Isso é facilmente implementado por uma LRU (Last Recently Used) em memória, não com objetos nativos do banco (javabeans), mas DTOs (Data Transfer Objects), que são comuns em ambientes enterprise (item 3, figura 1). Outra alternativa, seria utilizar uma LFU (Last Frequently Used) em memória, que marcaria os registros com maior propabilidade de acerto considerando uma média de acerto durante o tempo, e não os últimos utilizados. Mesclar essas duas opções para formar o elemento padrão poderia ser outra tentativa para minimizar o trabalho do usuário.

Quando esses recursos são utilizados, é necessário permitir ao usuário a seleção do restante dos registros, coisa simples de fazer ao providenciar um elemento como "Exibir todos..." (item 2, figura 2), que retornaria o restante dos registros. Mesmo nessa busca, outro cache LRU (ou LFU) poderiam ser utilizados, sendo a ordenação baseada também nesses conceitos (ordenar por LRU ou LFU ao invés de alfabeticamente). Isso tudo, além de reduzir a busca no banco à menos de - sei lá - uma centena de registros (ou mesmo a zero se os LRU e LFU forem bons), elevaria a taxa de acerto a algo superior a 50-60%, ou seja, uma interface 600x mais performática que a primeira.

Conclusões
Terminei dizendo que implementar esses recursos é legal porque a aplicação aprende com o usuário. Ela transforma-se em um ser vivo, e não apenas um monte de retângulos estáticos e coloridos na nossa frente.
Como consequência (não uso trema porque meu teclado é US, sorry), o Hibernate tem menos objetos para carregar e os problemas de tempo de carga simplesmente não ocorrem. Em outras palavras deixamos de perder tempo para resolvê-lo-os para ganhamos tempo (a) evitando-os e (b) dando ao usuário um sistema mais proativo.

Os dois analistas pareciam meio perturbados com o que eu falei. O terceiro, que tinha puxado o assunto, acabara de voltar do limbo para perceber que, novamente, seu café estava frio. Quando à mim, bem, pedi um chimas pro colega da frente, uma vez a nossa cuia estava gelada, idem...

0 comentários: