editorial_ Arquitetura: a Peça que Faltava nos Métodos Ágeis
O
movimento desencadeado pela ascensão dos métodos ágeis na engenharia de software trouxe várias práticas extremamente importantes para a qualidade de código. Essas práticas visam tornar o desenvolvimento de software sustentável, de forma que seja possível adicionar novas funcionalidades continuamente sem incorrer em altos custos para sua modificação e manutenção. Dentre essas práticas, se destaca a criação de testes automatizados, que contribuem para uma detecção precoce de problemas e dá segurança para realização de modificações. A cultura do código limpo, mantido com práticas de refatoração constante, é outra contribuição importante desse movimento. Porém alguma coisa estava faltando: a arquitetura do software! O Desenvolvimento Orientado a Testes, vulgo TDD, ajuda muito no design individual de cada classe, já criando-as de forma desacoplada de suas dependências. Porém, o TDD possui um foco local e não ajuda muito quando estamos buscando soluções de design mais amplas. Apenas com TDD, seria criada uma grande rede de classes desacopladas, porém ele não garante uma consistência global entre as soluções. Nesse momento é que a refatoração entra em cena! As classes criadas com TDD podem ir sendo refatoradas e uma reestruturação para manter uma consistência geral no software pode ser alcançada. A partir de pequenos passos, as transformações podem ir sendo realizadas de forma a tornar a estrutura homogênea e promover o reúso de código. Esse é o chamado design emergente! Porém, será mesmo que essa é a melhor solução? Será que vale a pena deixar para trás anos de conhecimento e experiência no trabalho com arquiteturas de software? Em metodologias tradicionais, perdia-se muito tempo no projeto, especificação e documentação da arquitetura e, talvez por esse motivo, os métodos ágeis romperam com esse paradigma de pensar em arquitetura antes de começar a implementação do software. Mas, parafraseando o título de uma palestra da Rebecca Wirfs-Brock, será que precisa correr ou apostar tudo? Será que não é possível trabalhar com o projeto da arquitetura de uma forma ágil? Os métodos ágeis em geral defendem que as atividades feitas em um projeto possuam o propósito de agregar valor ao produto final, diminuindo ao máximo a quantidade de desperdício. Nesses desperdícios inclui-se, por exemplo, a criação de documentos write-only e longas fases de análise para evitar mudanças que muitas vezes são inevitáveis. Porém, um projeto inicial de arquitetura não pode ser incluído nessa categoria! Muito pelo contrário, uma boa arquitetura promove o reúso de código e, consequentemente, diminui o tempo gasto em cada funcionalidade. Isso não é agregar valor? O projeto da arquitetura e a implementação de questões não-funcionais podem ir acontecendo de forma iterativa, porém é importante ter um ponto de partida que aponte a direção a ser seguida para os requisitos não-funcionais mais importantes. Isso complementa práticas como o TDD e a refatoração. A pouco tempo atrás começou-se a falar mais sobre práticas relacionadas a arquitetura para serem aplicadas em métodos ágeis, reconhecendo a importância delas existirem. Cada um tem exposto sua solução, e diferentes abordagens para se lidar com isso estão surgindo... Fique atento ao que está sendo dito e filtre o que se aplica aos projetos em que participa. Tome apenas cuidado ao descartar soluções que pareçam não ser adequadas a princípio, pois pode estar jogando fora conhecimento valioso! Lembre-se que o mesmo aconteceu com diversas outras práticas ágeis quando elas surgiram. “Com Arquitetura na ponta Ágil dos dedos!”
Eduardo Guerra editor-chefe @emguerra Siga o editor-chefe da MundoJ no Twitter e acompanhe em tempo real as novidades sobre a revista e sobre desenvolvimento de softwares em geral.
EDITOR EXECUTIVO | Marco Antonio Guapo EDITOR ADMINISTRATIVO | Osmar Zózimo de Souza Jr. EDITOR- CHEFE | Eduardo Guerra EQUIPE TÉCNICA | Alexandre Gazola, Breno Barros, Givanildo Santana do Nascimento, Guilherme de Azevedo Silveira, Paulo Silveira, Rafael Santos, Roberto Perillo, Rodrigo Cunha COLABORADORES DESTA EDIÇÃO | Paulo Silveira, Breno Barros, Eduardo Guerra, Ricardo Linden, Rafael Santos, André Grégio, Cézar Taurion, Sylvio Barbon Junior, Marcos Pedro Gomes da Silva, Fernanda Boaglio e Nicolas Gentille. PROJETO E DESENVOLVIMENTO GRÁFICO | Editora Mundo Adoro Design JORNALISTA RESPONSÁVEL | Débora A. R. Dias – DRT 3793 ARTIGOS Se você deseja escrever para a MundoJ, envie sua sugestão para artigos@mundoj.com.br CONTATO Revista MundoJ Caixa Postal 18.830 - CEP 80410-990 Curitiba - PR - Tel.: 41 3029.9353 PUBLICIDADE publicidade@mundoj. com.br COMERCIAL comercial@mundoj. com.br DISTRIBuIçãO ExCLuSIvA NO BRASIL Fernando Chinaglia Distribuidora S.A. Rua Teodoro da Silva, 907 CEP 20563-900 - Rio de Janeiro – RJ A revista MundoJ é uma publicação bimestral da Editora Mundo ISSN 1679-3978 O conteúdo dos artigos é de responsabilidade dos autores. Os softwares distribuídos via CD-ROM e encartes com a revista são de propriedade e responsabilidade de seus fabricantes, assim como o suporte e os direitos autorais.
_índice
NÚMERO 50 | ANO IX | NOVEMBRO & DEZEMBRO DE 2011 www.mundoj.com.br
08 Adotando Arquitetura Ágil em seu Processo de Desenvolvimento de Software
veja como adotar, modelar e documentar a arquitetura, de maneira ágil, em seu processo de desenvolvimento de software.
14 Práticas para Lidar com Arquitetura em Ambientes Ágeis
Aprenda as principais práticas que são utilizadas para lidar com arquitetura em empresas que adotaram com sucesso o uso de métodos ágeis.
28 Gerenciando a Cobertura de Testes em um Sistema
Como avaliar se todo o seu código está sendo verificado pelos seus testes? A resposta é: maximizando a cobertura de seu código pelos seus testes.
artigos > 36 Visualização Gráfica de Grafos com a API JUNG Aprenda como usar a API
JuNG para visualizar conjuntos de dados representados como grafos.
54 Comparando Persistência de Dados com JPA TopLink e Hibernate testados
em diferentes bancos de dados e sistemas operacionais.
58
Profiles no Spring 3.1 Melhore o
controle de suas aplicações usando os profiles do Spring 3.1.
62 Qualidade sob Medida com o PMD e o Eclipse De uma forma simples e sem muito esforço, crie suas próprias regras para validação de código Java.
colunas > 06 Tópicos Mais Quentes do GUJ.com. br. veja o que apareceu, foi notícia e
gerou discussão no fórum do GuJ durante setembro e outubro de 2011.
52 Tendências em Foco: Conhecendo o Hadoop uma das maiores invenções de data management desde o modelo relacional.
guj.com.br
notíc ias m
tópicos mais quentes do s ai
s lida
e comenta
guj.com.br
da s
Saída e morte de Steve Jobs http://www.guj.com.br/250853 http://www.guj.com.br/254508
Google: “não queremos especialistas“ www.guj.com.br/ 251396
Duas notícias ruins nesses dois meses: a saída de Steve Jobs como CEO e seu falecimento.
A idéia de que as empresas não devem procurar especialistas em tecnologias e linguagens específicas traz a discussão sobre boas faculdades, teoria X prática, evolução do desenvolvedor na empresa etc.
Java FX 2.0
Um dos www.guj.com.br/254294 principais Oracle retira licença para distribuir Java lançamentos com Linux, só resta usar o OpenJDK no JavaOne 2011, a notícia de uma http://www.guj.com.br/251394 nova versão do Java FX surpreendeu. Novos componentes, melhor documentação, plugins, outras linguagens e mais. Será que ainda Com os novos movimentos da Oradá tempo da Oracle abocanhar cle, apenas o OpenJDK fica como esse mercado? alternativa para já vir instalado nas distribuições do Linux. Com isso, novas profecias sobre problemas com a Oracle surgem.
/6
Paulo Silveira | paulo.silveira@caelum.com.br Um dos fundadores do GUJ.com.br, o maior fórum em língua portuguesa sobre a plataforma Java, nascido em 2002. É desenvolvedor e instrutor pela Caelum e editor-técnico da revista MundoJ.
Recursividade é um dos recursos que logo aprendemos, seja na faculdade, seja no trabalho. Apesar de ser um dos princípios básicos para facilitar a implementação de diversos algoritmos, muitas vezes aparecem dúvidas, e outras estouros de pilha. O Fibonacci é o exercício clássico:
co s tóp i
m
e lidos comentad s i os a
Aprendendo Recursividade http://www.guj.com.br/251258 Em um post polêmico, Sergio Oliveira discute alguns pontos que considera problemáticos do Hibernate. Alguns deles são considerados vantagens por outras pessoas.
Onde devemos, afinal, colocar o try/catch? Muita gente vai simplesmente relançando ou deixando passar a exceção para a chamada anterior, mas quando fazer o tratamento de um problema? No managed bean? No DAO? Varia?
Tratamento de exceções em camadas www.guj.com.br/253741
Você não gosta de hibernate? Eu também não. www.guj.com.br/252013
Migrando para .NET www.guj.com.br/252097
Muitos desenvolvedores Java acabam caindo em um projeto .NET: seja por mudanças na empresa, de projeto, cargo etc. Quais são os desafios?
ACOMPANHE O GUJ twitter.com/guj_noticias facebook.com.br/guj.com.br
Hiding/Shadowing de atributos pode confundir o programador, em especial quando há comparações com reescrita de métodos (não há de atributos). É uma dúvida frequente
Acessando atributo da superclasse www.guj.com.br/255342
FÓRUM EXCLUSIVO guj.com.br/mundoj Nesse fórum você pode conversar com os autores de cada edição da MundoJ, tirando dúvidas, colaborando, adicionando e criticando todos os artigos.
7\
capa_
Adotando
Arquitetura
ágil
em seu processo de desenvolvimento de software Veja como adotar, modelar e documentar a arquitetura, de maneira ágil, em seu processo de desenvolvimento de software. Sabendo que os aspectos arquiteturais são fatores que podem afetar todo um software em desenvolvimento, e não somente uma funcionalidade. Somando-se ao fato que a adoção de métodos de desenvolvimento de software incrementais e ágeis está cada vez mais em alta. Tem resultado em uma preocupação, ou interesse, cada vez maior em como realizar decisões arquiteturais de maneira incremental sem que elas afetem todo o software produzido até o momento. De fato, se não encontrarmos maneiras de incrementar e evoluir a arquitetura de forma flexível, segura e controlada, nada adiantará entregar porções de softwares rápidos, pois em algum momento, necessidades funcionais ou não-funcionais poderão surgir e a arquitetura atual não suportará ser alterada sem por em risco tudo que já foi desenvolvido. Ou seja, uma falha na arquitetura tornou-se agora uma falha de planejamento e entrega do projeto. Além disso, durante muito tempo viemos reclamando do problema em métodos tradicionais de se especificar funcionalmente todo o software antes de desenvolvê-lo, gerando uma paralisia da análise, e resultando muitas vezes em atrasos dos projetos de desenvolvimento de software. Entretanto, depois de vários esforços e técnicas para sanar essa paralisia da análise, o problema agora é outro: a Paralisia Arquitetural. O problema /8
não é mais apenas as variações de escopo de um projeto, mas sim se a arquitetura para atender a primeira entrega será suficientemente flexível para atender eventuais necessidades na décima entrega, por exemplo, sem gerar grandes retrabalhos ou até mesmo refazer todo o software. Com isto, as pessoas responsáveis pela arquitetura do projeto ficam cada vez mais inseguras se o que elas projetaram já está apto para o desenvolvimento da primeira entrega. Diante disto, se o método de desenvolvimento de software é ágil, por que a atividade de definição da arquitetura deverá ser feita toda no início do projeto, e não de forma incremental? Este artigo faz uma breve introdução sobre a Arquitetura Ágil e suas influências. Logo após serão apresentadas práticas de como aplicar a arquitetura ágil em processo de desenvolvimento de software, qual é o papel do arquiteto ágil, modelagem e documentação ágil.
As Influências da Arquitetura Ágil
Em métodos tradicionais, as atividades relacionadas à arquitetura gastam um tempo significativo no início do projeto para a definição da visão arquitetural, apenas do ponto de vista técnico. E, uma vez que esta visão tenha sido estabelecida, as equipes tendem a resistir às mudanças arquiteturais durante todo o ciclo de vida do projeto, muitas vezes, por não
Breno Barros | brenobarros@gmail.com | @brenoobarros Líder do Escritório de Arquitetura e Métodos Ágeis e do Centro de Excelência SOA da Stefanini IT Solutions. Além disso, atua na evangelização e desenvolvimento de práticas ágeis e Lean nos processos de desenvolvimento de novos produtos para start-ups de tecnologia.
As práticas de arquiteturas ágeis visam permitir a evolução do software de forma objetiva, rápida, controlada e colaborativa para garantir a entrega de softwares que atendam as variações de negócios cada vez mais frequentes. Este artigo visa apresentar os conceitos, influências, atividades e estratégia para adoção, modelagem e documentação de arquiteturas ágeis. entenderem que o cenário de negócio que aquele software é destinado mudou de um mês para o outro. E acreditem, em um mundo cada vez mais dinâmico e globalizado, isso já é comum e esperado. Não precisamos ir muito longe, imaginando modelos de negócios tão complexos, para acreditar nesse fato. Basta olharmos para as diversas start-ups tecnológicas que surgem a todo momento. Muitas das vezes, as mudanças de negócios neste segmento não mudam de um mês para o outro, mas sim de um dia para outro. E neste e em outros cenários, a arquitetura de software deve ser capaz de evoluir ou ser modificada a qualquer momento sem grandes impactos. É neste contexto dinâmico que a arquitetura ágil nasceu e vem ganhando cada vez mais força. Pois não devemos olhar a arquitetura como um aspecto técnico de um projeto de software, mas olhá-la como um componente crucial para a entrega de valor aos envolvidos no projeto. E por falarmos em desenvolvimento de software, valor de negócio, flexibilidade e entregas frequentes, nos vêm em mente um pensamento que vem cada vez mais influenciando a engenharia de software de ponta-a-ponta: o pensamento Lean, ou Lean Thinking. E não é à toa que o conceito de arquitetura ágil tem em seus pilares os princípios Lean. Assim, a arquitetura ágil visa Eliminar o Desperdício de tempo para a definição da arquitetura de software, buscando Tomar a Decisão o mais Tarde Possível, pois quanto mais tempo você adiar suas decisões, mais contextualizadas e assertivas elas serão. Porém isso não implica em não visualizar o Todo, mas sim que devemos estar diariamente antenados para tudo que está ocorrendo no projeto, inclusive o cenário de negócio que se destina, considerando futuras mudanças (e certamente elas vão ocorrer), para que quando elas surgirem, não surpreendam. Além disso, a arquitetura ágil foca no poder de decisões em conjunto (recordando o velho provérbio
que diz: “várias cabeças pensam melhor que uma”). Ou seja, a arquitetura ágil foca em dar Autonomia para a Equipe participar das decisões arquiteturais, como forma de melhorar não só as soluções técnicas, mas também a comunicação e Amplificar o Aprendizado, visando com isto Entregas mais Rápidas e uma Construção com maior Integridade, para gerar valor aos clientes. Sendo assim, a arquitetura ágil é uma técnica, ou conjunto de práticas, que uma equipe de desenvolvimento usa para criar um sistema de software. Ela envolve a tomada de decisões sólidas e oportunas em todo o ciclo de desenvolvimento de software. Em alguns casos, isso significa admitir que um erro arquitetural ocorreu e que a equipe precisa modificar a arquitetura.
Adotando uma Arquitetura Ágil de um Processo de Desenvolvimento
Até aqui, vimos o quão importante é a arquitetura de software independentemente se o método de desenvolvimento adotado é ágil ou não. Assim, sem radicalismos, a decisão se a arquitetura de software será realizada totalmente antes da codificação do projeto ou se ela vai evoluir incrementalmente, é uma decisão particular de cada equipe. Entretanto, para uma adoção real e cautelosa das práticas de arquitetura ágil, é recomendado uma adaptação entre os dois mundos: investir um pouco de tempo no início do projeto (comumente denominado de fase de Inception) para pensar nas “grandes necessidades”, e abordar os detalhes e tomar as decisões mais assertivas no momento certo (just-in-time) e responsável, ao longo das iterações (ou sprints) do projeto. A figura 1 ilustra bem a estratégia recomendada. No início do projeto, ou fase Inception, após o Product Owner já ter levantado um conjunto de requisitos do sistema em um nível abstrato (mas que
9\
capa_
Práticas para Lidar com
Arquitetura em
ambientes ágeis Aprenda as principais práticas que são utilizadas para lidar com arquitetura em empresas que adotaram com sucesso o uso de métodos ágeis. Arquitetura de software é um conceito difícil de ser definido, porém existe consenso a respeito de algumas características. A arquitetura de software envolve uma representação abstrata dos tipos de componentes de um software, de forma a definir a responsabilidade de cada um e como eles devem interagir. As decisões arquiteturais são aquelas que afetam todo o software e não somente uma funcionalidade ou pedaço. uma das dificuldades de se lidar com arquitetura de software em ambientes ágeis é que ela envolve decisões que afetam todo software. Sendo assim, pode ser muito arriscado ignorar certas questões arquiteturais em iterações iniciais, pois isso pode gerar um grande retrabalho depois. Diz-se que se deve lidar com essas decisões arquiteturais no “último momento responsável”, porém esse momento não é algo trivial de se descobrir. Daí surgem questões como: será que implementando certa funcionalidade da arquitetura antes das outras, não estou criando um design complexo demais antes da hora? Será que deixando uma funcionalidade para depois não vou gerar muito retrabalho? Será que trabalhando na arquitetura antes do código não estou deixando de ser ágil? Este artigo não tem o objetivo de apresentar uma solução definitiva para se lidar com arquitetura em ambientes ágeis, mas apresentar várias práticas que podem ser utilizadas para se lidar com decisões arquiteturais. Elas podem ser utilizadas todas juntas ou de forma individual. O fato de existirem essas práticas não significa também que elas representam a única forma de se lidar com arquitetura ágil. Essas
/ 14
práticas foram tiradas de experiências pessoais, de relatos de experiências e de conversas informais com amigos que já passaram por essas experiências. Sendo assim, representam práticas do mundo real e não apenas teorias que ninguém nunca experimentou. As seções seguintes apresentam seis práticas para arquitetura ágil e na última seção é mostrado como elas se relacionam e podem ser combinadas. Curioso para saber quais são as práticas? Então siga para a próxima seção!
Arquitetura Cartoon
uma das questões que envolvem a definição da arquitetura em um projeto ágil é o medo de se empregar muito esforço no desenho da arquitetura antes do projeto começar. Por outro lado, também é arriscado pular direto para o código sem pensar nas principais questões relacionadas com os requisitos não-funcionais, pois isso pode gerar um risco de retrabalho grande no futuro. Outro problema é os desenvolvedores começarem cada um a desenvolver sua própria solução para o mesmo problema na aplicação. Mesmo que o código em si não esteja duplicado, isso diminui o reúso de código dentro da arquitetura e a torna heterogênea. Mas qual é o limite? Será que é possível trabalhar em cima da arquitetura sem gastar muito tempo gerando documentações write-only (ou seja, que ninguém vai ler)? Como definir soluções arquiteturais para lidar com os principais requisitos não-funcionais e compartilhar esse entendimento com toda a equipe? uma prática que lida com esse problema é a cha-
Eduardo Guerra | guerra@mundoj.com.br | @emguerra Desenvolvedor de frameworks, pesquisador em design de software, editor-chefe da revista MundoJ e professor do ITA, onde concluiu sua graduação, mestrado e doutorado. Possui diversas certificações da plataforma Java e experiência como arquiteto de software nas plataformas Java SE, Java EE e Java ME. Participou de projetos open-source, como SwingBean e JColtrane, e acredita que um bom software se faz mais com criatividade do que com código.
Um dos principais focos do desenvolvimento ágil é a criação de um código de qualidade. Práticas comuns como TDD e refatoração possuem um impacto grande em porções locais do código, porém muitas vezes não atingem o objetivo de manter uma consistência na arquitetura de toda a aplicação. Desse fato, surgem questões a respeito do quanto se deve dedicar ao projeto da arquitetura antes da implementação ou quando se deve trabalhar em refatorações para adequar requisitos não-funcionais. O objetivo deste artigo é apresentar algumas práticas para lidar com arquiteturas ágeis. mada Arquitetura Cartoon. A Arquitetura Cartoon é uma prática na qual o arquiteto faz um desenho livre para a representação dos principais elementos da arquitetura. Além dos tradicionais retângulos, bolinhas e setinhas, também é recomendável inserir figuras ou ícones que representam de maneira mais visual os elementos e componentes definidos na arquitetura. » Para que fique mais claro como esse tipo de representação pode ser feito, vamos a um exemplo. Imagine uma aplicação em que o usuário precise solicitar relatórios que são gerados a partir de uma base de dados. Devido à complexidade dos relatórios e ao tamanho da base de dados, cada relatório pode demorar um tempo para ser processado. Nesse cenário, a partir de conversas com os clientes, foram identificados os seguintes requisitos: » O usuário não deve ficar aguardando para saber se sua requisição de relatório está sendo processada. » uma quantidade grande de relatórios complexos rodando ao mesmo tempo pode derrubar o servidor, então a arquitetura deve ter uma forma de controlar sua carga máxima de processamento simultâneo. » Relatórios gerados para um usuário devem poder ser recuperados novamente sem a necessidade de um novo processamento. Depois de pensar a respeito de como resolveria o problema, foi feito o desenho apresentado na figura 1. Acho que nesse ponto vale a pena ressaltar que demorei menos de 30 minutos para fazer esse dese-
nho. O investimento em tempo para a criação dessa arquitetura cartoon não deve ser muito grande. uma alternativa seria fazer esse desenho em um quadro branco, por exemplo. O ideal é tentar representar tudo que for mais importante em um único desenho, porém nada impede que sejam feitos dois ou três desenhos com focos em diferentes questões. Ressalto aqui que quanto maior for a quantidade de desenhos e detalhes relativos a questões marginais, menos atenção será dada ao que realmente é importante. A arquitetura cartoon deve focar nas soluções para os requisitos não-funcionais mais importantes para a arquitetura em questão. No exemplo apresentado, o foco foi em como os relatórios serão processados, armazenados e recuperados. O desenho não representa, por exemplo, como irá funcionar a segurança dentro da aplicação. O motivo é que isso não foi considerado um requisito crítico no momento e que não oferece muito risco de ser implementado depois. Em um sistema em que a assinatura digital de documentos fosse um requisito importante, por exemplo, certamente a representação da arquitetura incluiria algo a respeito. Note também que
15 \
Como embutir novos requisitos não-funcionais em uma arquitetura
Muitos dos requisitos não-funcionais representam características transversais do sistema, ou seja, que cortam toda a estrutura de funcionalidades. Isso significa que essas questões, como segurança e logging, estarão presentes em diversas funcionalidades do sistema. Para permitir que a implementação dos interesses transversais possa acontecer em paralelo e de forma independente das funcionalidades, eles devem estar de alguma forma modularizados. um padrão de projeto que nos permite adicionar novas funcionalidades em uma classe existente é o Decorator. Essa classe funciona como um proxy que implementa a mesma interface da própria classe. O Decorator delega a funcionalidade para a classe original, executando a funcionalidade adicional antes ou depois. Se as classes que quer encapsular possuem todas a mesma interface, a simples aplicação do padrão deve ser suficiente, porém quando existem várias interfaces é necessário apelar à API de reflexão e utilizar proxys dinâmicos. De qualquer forma, para essa estratégia poder ser utilizada, é preciso ter controle do momento da criação do objeto, como com o uso de uma fábrica, por exemplo. Assim é possível retornar o objeto encapsulado com o Decorator, ao invés do objeto original! Outra alternativa nesses casos é o uso de componentes que podem interceptar a execução de funcionalidades. A API de Servlets, por exemplo, possui os Servlet Filters que podem ser utilizados pata executar funcionalidades antes ou depois de uma requisição HTTP ao servidor. O EJB a partir da versão 3 e a nova API CDI possuem os interceptores, que podem ser utilizados pata interceptar a chamada de métodos nas classes controladas pelo container. Até mesmo a API de persistência como o JPA possui os listeners, que são chamados quando determinados eventos ocorrem no momento de persistir ou recuperar uma entidade. Todos esses componentes podem ser utilizados para adicionar novos comportamentos em funcionalidades já existentes de forma transparente. Finalmente, não poderia me esquecer dos aspectos que foram tema dos meus dois últimos artigos para a revista! Eles são módulos cujo objetivo é justamente a separação desses interesses transversais do resto da aplicação. Se sua aplicação utiliza o Spring, ele já vem preparado para o uso de aspectos em tempo de execução a partir da sintaxe de anotações do AspectJ. Se você não o utiliza, o AspectJ ainda permite que os aspectos sejam combinados com a aplicação no momento da compilação ou do carregamento das classes pela máquina virtual. Muitas vezes essas funcionalidades precisam ser configuradas e são ligeiramente diferentes para cada método invocado. No caso do controle de acesso, é preciso ter a informação de quem tem autorização para aquela funcionalidade. Nesse caso, uma saída é utilizar metadados adicionais na forma de anotações, armazenados em documentos xML ou no banco de dados. A partir dessas configurações, esses módulos têm informação suficiente para saber como a funcionalidade precisará ser executada. Independentemente da estratégia utilizada, a separação desses interesses deixa o código funcional mais limpo, mais fácil de ser reutilizado e mais fácil de ser testado. Além disso, isso permite a evolução dos serviços da arquitetura de forma independente do resto da aplicação.
LANDING ZONE
mínimo
alvo
excelente
Figura 2. Representação da Landing Zone.
/ 22
tar o que realmente é necessário para ela, evitando a inserção de coisas que nunca serão utilizadas. Deve-se trabalhar nas funcionalidades da arquitetura focando sempre em necessidades concretas e que são necessárias agora, como para user Stories da iteração atual. Isso evita especulações do tipo: ”Pode ser que um dia iremos precisar disso!”. Por outro lado, é importante criar esses serviços arquiteturais de forma fácil de serem reutilizados em outras funcionalidades. vamos ilustrar usando como exemplo uma funcionalidade de controle de acesso. Imagine que na terceira iteração de um projeto decide-se implementar o controle de acesso em uma funcionalidade onde isso é muito importante. Na ideia de fazer tudo da forma mais simples, isso poderia ser incluído como uma verificação “hard-coded” no meio do corpo de um método. Porém todos sabem que o
controle de acesso precisará ser feito para todas as funcionalidades e essa abordagem geraria retrabalho. Sendo assim, apesar do foco ser nos requisitos atuais de controle de acesso, deve-se tentar isolar a funcionalidade de forma a ela poder ser reutilizada. Nesse caso, poderia ser utilizado algo como um filtro, um interceptor ou um proxy (ver quadro). um serviço da arquitetura não precisa em sua primeira versão já atender a todos os requisitos possíveis. Ele pode começar simples, atendendo a necessidade do momento, e depois ir evoluindo aos poucos. O isolamento dessa funcionalidade do resto da aplicação permite que ela possa ser alterada facilmente, sem impactar em diversos pontos do código, caracterizando o bad smell Shotgun Surgery. O uso de testes de unidade ajuda a garantir que o caso mais simples continua funcionando com adição de comportamentos mais sofisticados. É preciso deixar bem claro que deixar espaço para que os requisitos não-funcionais possam ser incluídos e evoluídos aos poucos é diferente de querer implementar todos de uma vez. Quando a arquitetura é elaborada, é importante que já exista um plano de como essas novas funcionalidades que afetam toda aplicação poderão ser incluídas de forma isolada da parte funcional do sistema. É diferente criar um design especulativo, no qual se inclui coisas que não sabe se eram necessárias, de um design responsável, na qual se inclui pontos de extensão nos locais certos para que a arquitetura tenha a capacidade de evoluir. Saber a diferença entre os dois não é algo trivial e que vem da experiência dos membros do time. Felizmente errar é permitido e a refatoração existe exatamente para eliminar soluções desnecessárias e evoluir para novas estruturas.
Landing Zones (ou Zonas de Pouso)
Existem algumas características não-funcionais de uma arquitetura que não são serviços que cada uma das funcionalidades irá utilizar. um bom exemplo é desempenho! O tempo que um software demora para ser executado ou a memória que ele consome são consequência de sua implementação como um todo. Outras características não-funcionais como carga, disponibilidade e, em alguns casos, portabilidade também entram nessa categoria. Quando se deve trabalhar na arquitetura da aplicação para melhorar uma dessas características? Quando ela está boa o suficiente? Tive a oportunidade de assistir a palestra da Rebecca Wirfs-Brock no Agile Portugal deste ano, onde ela introduziu um conceito muito interessante chamado Landing Zone (ou Zona de Pouso). Em um software complexo, é complicado definir que um valor ótimo para uma determinada característica, principalmente porque muitas vezes é preciso balancear
essas características para se chegar a um resultado adequado. A Landing Zone é um intervalo de uma característica mensurável do sistema apresentando quais são os valores aceitáveis para o mesmo. A figura 2 apresenta graficamente a ideia do conceito. uma forma simples de se representar uma Landing Zone é através de uma tabela. Essa tabela possui uma coluna contendo o valor alvo para o valor da medição, um valor que seria excepcional e outra que seria o limite mínimo aceitável. Cada linha representa um atributo diferente que deve ser medido. No caso de valores que precisam ser medidos manualmente, o ideal é que a medição seja feita pelo menos uma vez ao final de cada iteração, para saber se será necessário incluir alguma atividade de ajuste da arquitetura para a iteração seguinte. Algumas medições podem ser obtidas de forma automatizada, de forma a poderem ser executadas em períodos mais curtos, como a cada dia ou a cada build.
Landing Zones para Qualidade de Código
Requisitos para qualidade de código são atributos que podem ser incluídos dentro de uma Landing Zone do sistema. A partir deles pode-se monitorar continuamente como anda a qualidade de cada módulo do sistema e realizar as devidas refatorações quando necessário. Ferramentas para inspeção e controle contínuo de código que podem ser utilizadas para o controle desse tipo de Landing Zone. um bom exemplo de uma ferramenta desse tipo é o Sonar, que foi apresentado com maiores detalhes no artigo “Evoluindo Design e Arquitetura Através das Métricas do Sonar” da edição 43 da revista.
A tabela 1 apresenta um exemplo de uma tabela com a definição de Landing Zones para o desempenho e a carga do sistema de relatórios que está sendo usado como exemplo neste artigo. Note que nem sempre o valor alvo fica exatamente na metade entre o limite e o excelente. No caso do tempo de processamento de um relatório o valor alvo é 30 minutos, sendo que o valor limite é o dobro, uma hora. O valor excelente já é de um minuto, 30 vezes menos do que o alvo. Em alguns casos, pode ser que o valor limite e o alvo sejam iguais, caracterizando uma situação em que o valor desejado já é o limite mínimo para aquele atributo.
23 \
necessários testes funcionais ou testes de integração. uma refatoração que adicionar uma nova camada precisará que os testes funcionais validem se o comportamento geral da aplicação permaneceu o mesmo. Em outra refatoração que alterar a relação entre dois subsistemas, um teste de integração entre eles seria suficiente. O tipo de teste utilizado vai depender muito do escopo da refatoração. Da mesma forma que a refatoração de código, a
para definir uma arquitetura abstrata ARQUITETURA CARTOON
para criar referência de como a arquitetura será testada ARQUITETURA DE TESTES
para validar decisões e definir arquitetura concreta
para verificar testabilidade da arquitetura USE STORY EXPERIMENTAL
para inserir questões não-funcionais incrementalmente
para monitorar atributos não-funcionais
LANDING ZONES
REQUISITOS NÃOFUNCIONAIS INCREMENTAIS
para ajustar atributos
para ajustar limites da Landing Zone
Juntando as práticas REAFATORAÇÕES ARQUITETURAIS
Figura 3. Relacionamentos entre as práticas apresentadas. / 26
refatoração arquitetural deve ser feita em pequenos incrementos. Por esse motivo, ela acaba muitas vezes sendo composta por diversas refatorações de código mais granulares até se chegar a um resultado com impacto em toda a aplicação. A cada pequeno passo da refatoração é importante rodar os testes para se certificar que até aquele ponto o comportamento se manteve. As Landing Zones podem ser utilizadas para detectar a necessidade de refatorações arquiteturais. Quando um atributo do sistema começa a sair dos limites do Landing Zone, é o momento de planejar uma atividade para fazer com que o valor daquele atributo volte para um valor desejável. Quando se utiliza métodos ágeis, deve-se procurar focar em questões que são importantes para o cliente e que agregam valor para o negócio. Teoricamente, ficar trabalhando na melhoria do desempenho da aplicação é algo que a princípio não agrega valor ao negócio e por isso essas atividades arquiteturais muitas vezes são deixadas de lado. Com as Landing Zones é possível detectar o momento em que aquele esforço para melhorar uma característica não-funcional do sistema realmente tem um impacto significativo para o negócio. As refatorações arquiteturais também podem fornecer um feedback em relação as Landing Zones definidas. Ao se refatorar a arquitetura, muitas vezes se faz trocas que melhoram um atributo e pioram outro. A criação de um cache, por exemplo, pode melhorar o tempo de resposta, mas irá aumentar a quantidade de memória consumida. A partir das refatorações, pode-se ir repensando os limites dos Landing Zones definidos, negociando com o cliente de acordo com a importância de cada requisito para o negócio. Nem sempre a alteração de uma característica geral da arquitetura demanda alterações em várias funcionalidades. Se aquela característica estiver desacoplada e isolada, muitas vezes a mudança pode alterar apenas uma parte específica, apesar dela ser executada a cada funcionalidade. Imagine que no exemplo dos relatórios a fila de processamento seja a princípio implementada com acesso ao banco de dados, mas os Landing Zones mostram que essa solução não atende o processamento mínimo de relatórios. uma refatoração arquitetural nesse caso seria utilizar JMS e um servidor de mensagens para implementar a fila e distribuir o processamento dos relatórios. Se o acesso a fila estiver isolado do código funcional, isso poderá ser feito sem precisar mexer em vários locais da aplicação.
As práticas apresentadas neste artigo podem ser utilizadas de forma independente uma das outras. Podem também existir outras práticas que complementem ou possam substituir as práticas apresen-
para saber mais/ Na edição 19, o artigo “Reflexão + Anotações — Uma Combinação Explosiva” apresenta situações em que a reflexão e anotações podem ser utilizadas para criar soluções mais inteligentes. A edição 32, o artigo “Proxys Estáticos e Dinâmicos” mostrou as técnicas que podem ser utilizadas para adicionar funcionalidades nas classes através de proxys. A sequência de artigos nas edições 23, 24 e 26, chamados respectivamente “Testes Unitários para Camadas de Negócios no Mundo Real”, “Testes de Unidade para Camadas de Persistência no Mundo Real” e “Testes de Unidade para Camadas de Apresentação no Mundo Real” apresentam técnicas para criar testes de unidade para diversas camadas de uma aplicação. Eles podem ser utilizados como referência para a criação da arquitetura de testes.
Considerações finais
Este artigo apresentou seis práticas para lidar com a definição, projeto e evolução da arquitetura de um software em um ambiente ágil. Apesar das práticas poderem ser utilizadas de forma independente, foi também apresentado como elas possuem papéis complementares desde a definição até a evolução de uma arquitetura. Em cada prática, foram apresentados quadros com algumas dicas a respeito de como elas podem ser implementadas ou com casos reais que podem servir como referência. É importante ressaltar que, assim como o xP e o Scrum, essas práticas servem como um ponto de partida e que, depois de implementadas, a equipe deve evoluir de acordo com as necessidades e seu modo de trabalho. Lembre-se que “se você tem um processo ágil hoje igual ao de um ano atrás, então você não deve ser mais tão ágil”, então não se prenda a nomes e práticas, assumindo a responsabilidade do seu processo de desenvolvimento.
Nas edições 46 e 48, os artigos “Programação Orientada a Aspectos para Leigos” e “Explorando Funcionalidades
/referências
de Static Crosscutting do AspectJ” apresentaram o uso de aspectos que podem ser utilizados para uma evolução transparente da arquitetura.
> Paul Clements, Felix Bachmann, Len Bass, David Garlan, James Ivers, Paulo Merson, Reed Little, Robert Nord, Judith Stafford. Documenting Software Architectures: Views and Beyond, Segunda Edição.
tadas. Cabe a cada equipe decidir o que utilizar ou não! Porém, as práticas apresentadas neste artigo apresentam certa sinergia quando utilizadas juntas, de forma que cada uma deve ser empregada em uma fase diferente e para um propósito diferente. Dessa forma, elas se complementam criando um método de lidar com arquitetura dentro de um projeto ágil de software. A figura 3 apresenta um mapa que mostra como as práticas se relacionam. A Arquitetura Cartoon é o primeiro passo, na qual se cria um esboço da arquitetura focando nos principais requisitos não-funcionais e de uma forma que facilite a comunicação com a equipe e com os clientes. Depois dessa fase, entra a definição da arquitetura mais concreta com a implementação de uma user Story Experimental, em paralelo com a Arquitetura de Testes. A criação dos serviços da arquitetura vai então ocorrendo de forma incremental com os Requisitos Não-Funcionais Incrementais. Durante o projeto é feito um acompanhamento dos atributos mais importantes do sistema através das Landing Zones. Quando um limite da Landing Zone é ultrapassado, isso motiva uma Refatoração Arquitetural para a correção. Através das refatorações pode-se ter uma noção mais realista dos requisitos do sistema e as Landing Zones também podem ser ajustadas.
> The Responsible Designer — “Don’t you want to take responsibility for your designs?” — Rebecca Wirfs-Brock — http://wirfs-brock.com/blog/ > Agilcast — “O podcast da AgilCoop!” — Episódio 12 — Arquitetura Ágil (parte 1) — http://ccsl.ime.usp.br/ agilcoop/files/Agilcast12-Arquitetura%20Agil%20-%20 parte%201.mp3 e Episódio 13 — Arquitetura Ágil (parte 2) — http://ccsl.ime.usp.br/agilcoop/files/Agilcast13Arquitetura%20Agil-parte2.mp3 > Gerard Meszaros. xUnit Test Patterns: Refactoring Test Code > Framework MakeATest – https://github.com/ marcusfloriano/makeatest-core > Kerievsky, Joshua. Refactoring to Patterns. > Muller, Gerrit. From Legacy to State-of-the-art; Architectural Refactoring. > Stal, Michael. Software Architecture Refactoring. http:// www.sigs.de/download/oop_08/Stal%20Mi3-4.pdf > Michael Stal on Architecture Refactoring — http:// www.infoq.com/interviews/michael-stal-on-architecturerefactoring
27 \
capa_
Gerenciando a
Cobertura
de testes em um sistema Como verificar se todo o seu código está sendo verificado pelos seus testes? A resposta é: maximizando a cobertura de seu código pelos seus testes. Hoje em dia os bons desenvolvedores já se convenceram de que têm que desenvolver testes unitários para as classes que desenvolvem. Entretanto, a pergunta é: estes testes são eficazes? Eles realmente verificam todas as partes do seu código? Neste artigo vamos discutir uma ferramenta que analisa esta cobertura e ideias para fazer com que seus testes realmente analisem todo o seu código.
O
conceito de testar seu código através de testes unitários já está bem disseminado entre os bons desenvolvedores. Todos concordam que os testes unitários fornecem uma garantia de que todo código desenvolvido funciona como esperado, permitindo a refatoração de seus sistemas sem a perda do comportamento correto. Os conceitos colocados no parágrafo anterior independem do fato de você usar desenvolvimento baseado em testes (veja o quadro). Eles simplesmente dizem que se você é um bom profissional, então você se preocupa com o produto que você entrega e deseja que ele satisfaça os requisitos de seu cliente. A única maneira de ter certeza de que seu sistema efetivamente vai fazer o que se espera dele é testando – e os testes unitários são uma parte muito importante do seu código. Como todos os textos que falam de metodologia ágil nesta edição vão deixar claro, refatorar código é uma necessidade real. Nós vamos falar um pouco mais sobre isto na seção intitulada “Cobertura e Refatoração”, neste artigo. Entretanto, neste momento é importante entender que não importa a sua qualidade como programador, uma destas situações será realidade em sua vida: » você vai precisar atender um novo requisito que sua estrutura de classes não previa; » você precisará melhorar a clareza do código
/ 28
gerado para facilitar a manutenção; vai precisar aumentar a velocidade de seu código; » vai encontrar um usuário que faz uma sequência de ações absurda; » vai ver seu módulo ser ligado a outros que não fazem testes nos parâmetros que lhe passam; » etc. uma vez que vai ter que enfrentar uma refatoração, temos que garantir que nosso sistema faz o que desejamos. Para isto, nós fazemos os testes. Entretanto, uma pergunta muito importante se impõe: como você sabe que seus testes estão efetivamente testando todo o seu código? Como saber se todos os casos de uso estão efetivamente sendo verificados? Será que aquele seu “else” no final do método está funcionando direito? Seus testes passam por ele? As dúvidas colocadas acima são resolvidas pelo conceito de cobertura de testes. Cobertura é uma medida de abrangência dos testes, que pode ser medida através dos casos de uso ou das linhas de código desenvolvidas, isto é, podemos verificar se estamos efetivamente testando todos os casos de uso ou se estamos testando todas as linhas de código de nosso sistema. O objetivo deste texto é ajudá-lo a analisar o segundo tipo de cobertura, isto é, a cobertura de linhas de código desenvolvidas. Existem ferramentas auto»
Ricardo Linden | rlinden@pobox.com é formado em Engenharia de Computação pela PUC-RJ e tem doutorado em Engenharia Elétrica pela COPPE-UFRJ. Trabalha com Java há cerca de 8 anos, desenvolvendo aplicativos para o setor elétrico no CEPEL. Atualmente é professor da disciplina de programação com qualidade na Faculdade Salesiana Maria Auxiliadora (FSMA), de Macaé-RJ e mantém um twitter sobre os conceitos de conduta profissional (@BomProfissional).
máticas para realizar esta análise, que pode ser de grande valia para que você possa entregar para seus clientes sistemas mais confiáveis e com menos bugs. Neste texto, teremos vários fragmentos de listagens. Por questões de espaço, não podemos colocar as classes completas. Entretanto, todos os códigos descritos neste artigo estão disponíveis no formato de projeto do Netbeans 6.9 no diretório http://www. algoritmosgeneticos.com.br/cobertura /index.html.
Instalando a ferramenta de análise de cobertura
uma das primeiras ferramentas gratuitas para verificação de cobertura de testes foi a Emma (http:// emma.sourceforge.net/). Quando do seu desenvolvimento, as ferramentas existentes eram usualmente caríssimas, o que fazia com que a maioria dos usuários não as utilizassem. Esta ferramenta foi incorporada nas duas principais IDEs do mercado: o Netbeans e o Eclipse, podendo ser facilmente instalada e utilizada, de forma gratuita. Para instalar esta ferramenta no Netbeans, vá ao Menu Ferramentas, opção Plugins e selecione a tab Plugins disponíveis. Neste momento, você deve ver uma tela similar à figura 1. Selecione então a opção Coverage e clique no botão Instalar. Responda a todas as opções tradicionais de licença e você terá instalado o software. Simples e direto. Podemos agora usar esta ferramenta para analisar nossos testes. Para tanto, vamos começar com um pequeno exemplo para que você efetivamente entenda o problema que estamos enfrentando.
Um exemplo simples
Para que nós possamos compreender o conceito efetivo de cobertura, vamos analisar o código da Listagem 1. Eu sei que o código é bastante “tosco” e merece uma refatoração extensiva1, mas ele serve
para nossos propósitos.
Listagem 1. Um simples método que retorna uma
string dizendo qual dos elementos passados como parâmetro é o maior. public class Utilidades { public String maior(int a, int b) { String retorno=””; if (a>b){ retorno=”O maior dos elementos é o primeiro (“+a+”)”; } else { retorno=”O maior dos elementos é o segundo (“+b+”)”; } return(retorno); } }
Como somos pessoas sérias, vamos criar um teste unitário para verificar este código. Podemos fazê-lo manualmente ou usando as ferramentas automáticas de nossa IDE. No caso do Netbeans, isto é conseguido clicando com o botão direito em cima do pacote de testes, selecionando a opção Novo e a sub-opção Testes para Classes Existentes. Depois de escolhermos uma classe e clicarmos em finalizar, receberemos. Ao recebermos uma grande classe com os métodos para testar cada um dos métodos existentes representados por stubs. Assim, para criar um teste todo o código que o Netbeans colocou (afinal, é apenas um stub, código colocado para ocupar espaço) e substituir pelo código da Listagem 2.
Listagem 2. Um método de teste para o código desenvolvido na Listagem 1.
Figura 1. Tela vista quando selecionamos a opção de instalação de novos plugins no Netbeans.
@Test public void testMaior() { System.out.println(“maior”); Utilidades instance = new Utilidades(); String result = instance.maior(3, 2); assertEquals(“O maior dos elementos é o primeiro (“+3+”)”, result); } 1 Fica como exercício para nossos leitores descobrir maneiras mais eficientes de implementá-lo. Aceito sugestões por e-mail! 29 \
Clicamos então com o botão direito sobre a classe utilidadesTest e selecionamos a opção Executar Arquivo. Ao final, recebemos a maravilhosa tela vista na figura 2, e nos sentimos maravilhosamente bem por termos feito testes unitários para nosso software e sermos boas pessoas que entregarão um programa eficaz para nossos clientes.
Figura 2. Tela vista quando executamos nosso método de teste. Como podemos ver, nosso software passou no teste que fizemos. Isto quer dizer que ele sempre funciona?
Entretanto, a pergunta é: como saber se efetivamente testamos tudo que deveríamos? A resposta a esta pergunta é muito simples: use a ferramenta de cobertura. Ela vai analisar se seu programa foi testado de forma conveniente ou não (ao menos sobre termos testado todas as possibilidades existentes em nosso código). vamos então verificar se nosso código está efetivamente testado. Para tanto, nós primeiramente clicamos no nome do projeto, selecionamos a opção Coverage e a subopção Activate Coverage Collection (vemos mais um exemplo de como a internacionalização de software pode ser difícil – apesar de meu Netbeans ser em português, as opções de análise de cobertura de código são todas em inglês). O próximo passo é muito simples: rode todos seus testes. Para tanto, clique com o botão direito sobre o seu projeto e clique na opção Teste. você verá que as mensagens de saída incluem um monte de linhas começando com “EMMA”, indicando que seu projeto está tendo sua cobertura analisada. Ao final do processo, você verá um relatório de testes habitual. Entretanto, uma coisa terá mudado: se você abrir qualquer um dos seus códigos-fonte, você verá que partes dele estão pintadas de verde, como vemos na figura 3. Estas partes são aquelas que são executadas durante os testes. Aquelas que não estão pintadas não foram testadas e a pergunta que você deve fazer agora é: o que eu faço com elas?
Figura 3. Aparência de nosso código depois de executarmos nosso teste com a opção de análise de cobertura habilitada. Note que a opção do else não está com fundo verde, o que indica que ela não foi testada por nossos testes unitários. / 30
Além da análise linha a linha, você pode obter um relatório sobre a cobertura de suas classes. Para tanto, selecione o seu projeto com o botão direito do mouse, escolha a opção coverage e a subopção Show Project Coverage Statistics. você verá um relatório similar ao mostrado na figura 4. Neste relatório você verá o número de linhas cobertas e não cobertas de cada classe do sistema, assim como uma percentagem de cobertura de cada classe. Na figura 4 podemos ver que a nossa classe utilidades tem apenas cinco de suas seis linhas cobertas. Neste momento você deve estar com uma “grande coceira” para ampliar a cobertura de seus testes e fazer com que seus sistemas tenham 100% de cobertura. O primeiro objetivo é nobre e deve ser realizado o mais rapidamente possível. O segundo é provavelmente desnecessário, como discutiremos na seção “Nem sempre seja 100%”. Antes de discutir estas questões, vamos avançar um pouco mais no conceito de ampliação da cobertura de forma a melhorar a segurança que temos em relação à correção de nosso software.
Figura 4. Aparência do relatório de estatísticas de cobertura fornecido pela ferramenta do Netbeans. Note que nossa classe Main tem 0% de cobertura (o que é muito razoável, posto que não desenvolvemos nenhum teste para ela) e a classe Utilidades tem uma linha descoberta, equivalente ao que vimos na figura 4, de forma mais detalhada.
Ampliando a cobertura
A nossa reação imediata aos resultados relatados na seção anterior é propor uma solução para o problema que consiste em descobrir quais linhas não estão cobertas pelos testes e escrever novos testes que façam com que as testemos também. No caso do nosso pequeno exemplo, temos o acréscimo mostrado na Listagem 3 para fazer os testes.
Listagem 3. Nossos testes com um acréscimo (marcado em negrito), de forma a testar as linhas que foram deixadas de fora na tentativa anterior. @Test public void testMaior() { System.out.println(“maior”); Utilidades instance = new Utilidades(); String result = instance.maior(3, 2); assertEquals(“O maior dos elementos é o primeiro (“+3+”)”, result);
ma e relativamente comum em testes de elementos. Acrescentando o teste, nós veremos que chegaremos ao else de nosso método e obteremos o resultado de que o maior é o segundo elemento. Entretanto, é isto } mesmo que nós queremos? Ou será que queremos Rodando o teste da mesma maneira que fizemos uma mensagem indicando que os dois elementos são na seção anterior, temos a felicidade de encontrar iguais? 100% de cobertura em nossa classe utilidades, como podemos ver na figura 5. uma pergunta razoável é: será que obter 100% de cobertura resolve todos os nossos problemas? Será que isto é uma panaceia para seus problemas computacionais? A resposta a esta pergunta é que apesar do objetivo de obter 100% de cobertura das linhas de código ser louvável e algo que efetivamente deve estar em nossos corações, ele não é o único objetivo que teremos quando escrevermos testes. Nós precisamos também procurar ter 100% de cobertura nos casos de uso de nosso sistema. Infelizmente, para isto, não existe uma ferramenta automática na nossa IDE para nos ajudar – nós temos que conversar com nossos clientes e ouvir o que eles têm a dizer para poder entender suas necessidades e desenvolver os casos de uso competentes. O box intitulado Desenvolvimento Figura 5. Aparência do código e do relatório de estatísticas de coIterativo discute um pouco como isto pode ser feito e bertura fornecido pela ferramenta do Netbeans depois de acrescomo isto pode nos ajudar a ter softwares que efeti- centarmos o novo teste. A classe Main continua com 0% de cobervamente fazem o que o cliente quer (e ganharmos a tura, mas a nossa classe Utilidades passou a ser 100% coberta. fama merecida de bons profissionais). Esta questão não é de correção de código, mas No caso de nosso pequeno exemplo, nós es- sim de verificação dos requisitos do usuário. Assim, quecemos de fazer um teste para o caso em que os você deve conversar com seu usuário antes, durante dois números são iguais. Esta é uma situação legíti- e depois da codificação, de forma a criar testes que result = instance.maior(2, 3); assertEquals(“O maior dos elementos é o segundo (“+3+”)”, result);
Desenvolvimento interativo
O desenvolvimento iterativo consiste em criar nosso software através de um processo evolucionário em que ele cresce através do incremento gradual. Cada iteração é determinada através do diálogo entre clientes e desenvolvedores (que muitas vezes trabalham juntos em um mesmo ambiente, em uma equipe multifuncional). O Manifesto Ágil (Beck et al, 2001) promove este conceito através da preferência de interações entre os indivíduos envolvidos no projeto e a interação com o cliente de forma a transformar o desenvolvimento de software em algo participativo que gere um resultado mais satisfatório para o cliente final (o que deve ser garantido pela sua participação mais intensa no processo inteiro). Entenda que o código que você desenvolve é apenas uma ferramenta para que as pessoas atinjam seus objetivos, tanto de negócio quanto pessoais. Assim, se você conversar com o destinatário final do seu aplicativo você terá mais chances de satisfazê-lo. O desenvolvimento iterativo preconiza a maximização da conversa com os usuários, através da criação de uma série de casos de uso a cada conversa. Estes casos serão entregues aos poucos, de forma a serem avaliados pelo cliente, de forma que ele perceba o desenvolvimento do sistema, avalie sua qualidade e participe bastante do processo de seu desenvolvimento. Sistemas que são desenvolvidos de forma iterativa tendem a satisfazer mais os clientes e a diminuir a necessidade de alteração e manutenção dos produtos entregues. Não é uma panaceia para o processo de desenvolvimento, mas sim uma constatação do princípio de escutar as pessoas para entender quem elas são e o que elas precisam. Siga este princípio tanto na sua vida profissional quanto na privada e suas relações com as pessoas que o cercam serão mais satisfatórias.
31 \
várias outras situações relevantes que devem ser tesreflitam aquilo que o usuário efetivamente deseja. Lembre-se: não basta seu código compilar e exe- tadas. Por exemplo: » O comportamento do método quando é passacutar sem bugs – ele não estará correto senão satisfido um vetor igual a null ou vazio é o esperado? zer os requisitos estabelecidos pelo usuário, fazendo você pode ter definido que seu método lança aquilo que ele deseja/necessita. uma exceção, retorna false ou não faz nada – isto é uma decisão que depende de sua equipe e Bons testes seu processo de programação. Entretanto, seu A primeira questão importante é saber o que tescódigo deve responder como esperado. No caso tar. Como estamos colocando neste artigo, seu pride nosso método simples teremos uma exceção meiro objetivo é ampliar a cobertura tanto das linhas em tempo de execução em ambos os casos. Será de código quanto dos casos de uso. Existem vários que era isto que desejávamos? livros que podem lhe ajudar a definir seus testes de » O que acontece quando seu vetor tem apenas forma a verificar seu código de forma completa. um um elemento? No caso de nosso método, o redos mais interessantes é (HuNT & THOMAS, 2004). torno é zero. Será que que queríamos zero mesOs princípios fundamentais de um teste é basimo ou queríamos o valor do único elemento? camente ele ser completo em relação aos casos de » Se passamos um vetor com dois elementos e uso e as condições que seu código deve enfrentar. tentamos pegar o último ou o primeiro, ele enLembre-se de que as ferramentas que colocamos aqui tende que não “tem nada entre eles”? Isto não são voltadas para a análise da cobertura das linhas se aplica ao nosso método, mas pode ser aplide código, mas que isto não é o objetivo único de seu cável a outras situações que você enfrente. código. » um vetor verdadeiramente enorme (com um Para entender esta questão, vamos colocar uma milhão de elementos, por exemplo) gera algum situação hipotética em que temos um método que tipo de exceção em tempo de execução? Lemmanipula um vetor de inteiros, como aquele que vebre-se apenas de que este último teste deve ser mos na Listagem 4. rodado de forma controlada, pois ele pode fazer Listagem 4. Código que determina a diferença entre o seu teste demorar demais e desmotivar sua o maior e menor elemento de um vetor. equipe a rodar os testes frequentemente. Entretanto, sua demora não minimiza sua imporpublic static int tância. determinaDiferencaEntreLimites(int[] v) { Esta lista não é exaustiva – é apenas uma mostra Arrays.sort(v); de que você deve analisar com cuidado todas as situreturn v[v.length-1]-v[0]; ações que cada um dos seus métodos vai enfrentar. } Lembre-se de que a ferramenta de cobertura é muito importante, mas não é o seu objetivo único e/ou úlSe você criar uma série de testes que sejam batimo. seados em um vetor grande (com 50 elementos, por Entretanto, existe outra questão importante a ser exemplo), como aquele que mostramos na Listagem considerada. uma vez que tenhamos tudo testado, 5, você vai conseguir chegar a 100% de cobertura desainda assim não necessariamente nós temos testes te método (verifique!). Entretanto, a pergunta é: você que podem ser efetivamente considerados como sentestou o seu método de forma adequada? do de alta qualidade. Existe uma noção equivocada de que os testes devem simplesmente rodar, não tendo Listagem 5. Um teste que gera 100% de cobertura qualquer requisito maior de legibilidade ou qualidade no método e que passa com sucesso. Será que ele – o importante é o teste funcionar. testa nosso método completamente? Os testes são uma importante ferramenta de do@Test cumentação do seu código. Tendo em vista que seu public void testDiferenca() { código deve ser testado para todos os comportamenSystem.out.println(“Diferenca”); tos que vai enfrentar e pressupondo que você seja um int[] v=new int[50]; desenvolvedor disciplinado, então todas as pessoas for(int i=0;i<50;i++) { que encararem seu código poderão olhar para seu v[i]=i; } conjunto de testes e ver o que seu código efetivamenint diferenca=Utilidades. te faz. determinaDiferencaEntreLimites(v); Este comportamento deveria ser esperado da doassertEquals(diferenca,49); cumentação, como os javadocs e outros documentos } escritos. Entretanto, todos aqueles que trabalham A resposta é não. Para testar um vetor, existem com código há algum tempo sabem que existe uma / 32
grande tendência a deixar a documentação desatualizada. Infelizmente, esta é uma realidade inexorável que permeia todos os projetos – as pessoas tendem a considerar que os processos de documentação são menos relevantes. Assim, você pode contribuir para a melhor compreensão do seu código fazendo testes que sejam facilmente compreensíveis pelos leitores. Martin (2008) oferece uma série de normas simples para guiar seu desenvolvimento de testes que, se seguidos, farão com que os testes (que você já desenvolverá para garantir o funcionamento do seu código) sejam uma ferramenta de autodocumentação. Basicamente o conceito é tratar os seus testes da mesma maneira que você trata seu código – procure maximizar a legibilidade seguindo os mesmos princípios que guiam seu desenvolvimento. Da mesma maneira que seu código, os seus testes também deverão ser lidos e alterados no futuro. Entenda, é claro, que estes princípios não são algo “inquebrantável” – se você os segui-los existe a tendência de que seus testes sejam mais legíveis, mas é possível fazer testes legíveis sem segui-los. Por exemplo, um dos princípios que Martin aponta é o princípio da responsabilidade única (SRP). Entretanto, eu acho muito chato desenvolver 20 ou 30 métodos diferentes para testar um único método. Assim, tenho a tendência a colocar todos os testes em um único método e colocar uma linha de comentá-
rio explicitando a função de cada pedaço. Não segue os princípios consagrados pela comunidade mundial, mas funciona adequadamente para mim e para as equipes com as quais trabalho. Não digo que eu sou um exemplo de conduta para todos os desenvolvedores, mas sim que você deve ler todos os livros e artigos sobre metodologia e adaptá-los à realidade em que você está inserido. Lembre-se apenas de seguir dois conceitos fundamentais: teste tudo de forma completa e faça testes bastante compreensíveis e manuteníveis.
Nem sempre seja 100%
A primeira ideia de qualquer desenvolvedor é que temos que desenvolver nossos testes com o objetivo de ter 100% de cobertura do nosso código. Entretanto, este número raramente será atingido (eu, pessoalmente, nunca o atingi, apesar de buscar desenvolver o máximo de testes para garantir a qualidade dos sistemas que desenvolvo). Existem vários motivos para que isto aconteça. Primeiramente, você não vai testar código que seja gerado automaticamente pela sua IDE. Por exemplo, ao desenhar uma tela no Netbeans, uma centena ou mais de linhas podem ser geradas. Apesar de teoricamente precisarmos testar isto, na prática é raro que uma IDE que está sendo usada por milhões de pessoas no mundo inteiro gere um erro em questões tão simples. Assim, se você não as testar, você não estará cometendo nenhum pecado capital. Em segundo lugar, certos métodos triviais, como métodos get que não têm código nenhum além de um return não necessitam ser testados em um primeiro momento. É claro que as melhores práticas sugerem que você os teste também, pois amanhã eles podem deixar de ser algo trivial e aí você já estará coberto. Entretanto, na prática, muitos desenvolvedores os deixam de lado. Certos trechos de códigos, como os de interface gráfica ou de comunicação com a rede, só podem ser testados com ferramentas específicas. Neste caso, não necessariamente sua ferramenta de cobertura se integrará com o objeto mock ou qualquer outro artefato que usar. Assim, mesmo que você efetue o teste sua ferramenta de cobertura pode não aumentar o número de linhas cobertas. O ponto fundamental é: seu objetivo deve ser uma aplicação que funciona e é confiável. Não se concentre na metodologia ou na ferramenta – elas são apenas instrumentos para que você entregue algo que vai efetivamente ajudar o usuário final a atingir seus objetivos (pessoais ou de negócio).
Cobertura e refatoração
Refatoração é definida no site de Martin Fowler (http://refactoring.com/) como uma técnica para rees33 \
truturar um corpo de código existente alterando sua estrutura interna sem alterar seu comportamento externo. Apesar desta frase ser complicada, ela basicamente quer dizer que nós vamos melhorar o código existente sem causar qualquer mudança no seu funcionamento. Até mesmo os principais evangelizadores do código limpo, como Robert Martin, preconizam que sua primeira preocupação é fazer o seu código funcionar. Depois, você deve preocupar-se com conceitos como legibilidade e manutenibilidade. Para isto é que precisamos dos conceitos de refatoração. você deve estar pensando neste momento: “eu sou um bom programador – meu código já sai excelente de primeira”. Infelizmente, por melhor que você seja – esta frase não é verdade. Praticamente todo código existente pode ser refatorado para melhorar a sua legibilidade e, consequentemente, aumentar sua manutenibilidade. Existe toda uma lista de “code smells” (literalmente, fedores de código) que usualmente estão escondidos dentro de nossos códigos (uma pequena lista pode ser encontrada em Martin (2008)). Ao remover cada um deles fazemos uma pequena mudança na estrutura de nosso código, com o intuito de melhorá-lo. A pergunta que resta é: como podemos garantir que nosso código ainda funciona? Neste momento, a ampla cobertura de código (garantida por testes que satisfazem os critérios que colocamos anteriormente) é a principal garantia que você tem. Ao fazer uma refatoração, seu passo seguinte deve ser rodar todos os testes que você tem (em uma IDE como o Netbeans ou o Eclipse, você pode fazer isto com um único comando). A refatoração só estará terminada quando você voltar a ter a barrinha verde de sucesso como resultado. Assim, ao criar seus testes é recomendável utilizar uma ferramenta de análise de cobertura para ter certeza que está verificando todos os casos possíveis de seu programa. Entenda que a refatoração é a precondição para que seu código efetivamente seja de boa qualidade e facilmente manutenível. Assim, sabendo que vai ter que mexer no seu código no futuro, você só poderá fazê-lo com segurança se seus testes efetivamente verificarem todas as possibilidades existentes no seu código (e também todos os casos de uso necessários para seu cliente). Nós já passamos da época em que frases como “compilou, está validado” ou “funciona aqui no meu ambiente” são aceitáveis. Lembre-se que você está entregando ferramentas de importância fundamental para os outros e deve ser respeitoso. Realizar as atividades de testes e de refatoração é uma demonstração deste respeito e dos conceitos de profissionalismo que você sempre deve desejar transmitir. Saber refatorar código é a diferença entre o pro-
/ 34
gramador que faz código que funciona e código que é de qualidade e altamente manutenível. Quem quiser conhecer mais sobre as principais técnicas de refatoração deve ler o principal livro sobre o assunto (FOWLER, 1999) e terá em suas mãos o poder de transformar nosso código “macarrônico” em algo que terá uma sobrevida muito maior.
Cobertura e desenvolvimento orientado a testes
O desenvolvimento orientado a testes (Test Driven Development, ou TDD) é uma técnica de desenvolvimento que procura primeiro criar os testes e depois desenvolver o código necessário para atendê-los. O desenvolvimento torna-se então um ciclo que tem os seguintes passos: 1. Desenvolva um teste para um determinado caso de uso que faça o código existente falhar. Lembre-se de que a primeira lei do TDD preconiza que não se deve desenvolver nenhum código antes de termos desenvolvido testes unitários e a segunda lei diz que desenvolveremos apenas o necessário para fazer o seu código falhar. 2. Escreva somente o código necessário para fazer com que este teste não falhe mais. Note que escrever mais do que o código necessário, você estará violando a terceira lei do TDD. 3. Se necessário, refatore o código para que o design permaneça claro e eficiente. Isto vale tanto para seu código quanto para os testes, pois estes, como vamos discutir na seção “Bons Testes”, devem seguir os mesmos princípios de qualidade que o seu código. 4. Se todos os casos de uso já foram devidamente testados e efetivamente desenvolvidos, encerre o desenvolvimento. 5. Caso contrário, volte ao passo 1. Ao fim do ciclo, teremos uma suíte de testes que verifica todos os casos de uso especificados e o código está pronto para ser refatorado ou devidamente modificado, se necessário (veja a seção Cobertura e refatoração para ver uma discussão sobre esta necessidade). TDD permite a criação de código de forma incremental e simples, devidamente testado e com conceitos que tendem a ser bastante simples, pois cobrem os conceitos pontualmente. Como os programadores fazem o código de forma a fazer funcionar cada um dos testes existentes, então a depuração tende a ser bastante simples. Outra vantagem importante é que o desenvolvimento dos testes obriga o programador a entender os casos de uso, de forma a gerar testes que sejam realistas e reflitam as necessidades expressas nas histórias de usuário.
Ivan Sanchez (2006) coloca em seu site que TDD não consiste só em desenvolver os testes antes do código, mas também pensar no design do código de forma incremental (como o passo 3 descrito acima procura deixar claro). Não procure refatorar de forma extensa neste momento, mas sim eliminar alguns dos erros mais comuns como duplicação, números mágicos e outros. Como colocaremos mais adiante neste artigo: a refatoração será uma necessidade imperativa ao fim do processo (e isto não quer dizer que você é um mau programador). Quando usamos o desenvolvimento orientado a testes, o uso de ferramentas de cobertura de testes tem importância diminuída, pois se o código é estritamente escrito com TDD, neste caso não haverá linhas de código de produção que não tenham sido cobertas por um teste (já que o código é feito para que o teste que falha passe a ser bem-sucedido). Isto não quer dizer que você deve ignorá-las, mas sim que provavelmente elas vão gerar o 100% que você espera. No caso de usar desenvolvimento orientado a testes, o mais importante é o outro aspecto dos testes – a cobertura de todos os casos de uso que seu usuário pode precisar. Assim, a análise e a conversa com o usuário torna-se um aspecto ainda mais fundamental do código bem-sucedido.
Considerações finais
Testes são fundamentais para garantir que um sistema faz exatamente o que é esperado. Em um ambiente profissional não se pode esperar que um cliente aceite que você se comporte como naquele comercial em que um paraguaio falava “la garantía soy yo”. Ao desenvolver testes, você deve procurar fazê-lo de forma que todas as possibilidades de caminho por seu código sejam devidamente testadas. Para isto, as ferramentas de análise de cobertura são de grande valia, podendo apontar de forma gráfica e integrada à sua IDE quais partes não foram devidamente testadas. Lembre-se, entretanto, que a cobertura não é exaustiva em termos de testes a realizar. É possível, como mostramos aqui, que você tenha 100% de cobertura e alguns casos de uso não tenham sido devidamente testados. Assim, você deve fazer com que seus testes, além de maximizar a cobertura, busquem cobrir todas as situações que podem ser encontradas na operação real do sistema. Outro ponto fundamental é que a cobertura não é um objetivo por si só. Não é obrigatório ter 100% de cobertura – existem várias situações que não requerem testes ou cujos testes não agregam valor ao seu sistema. Não fique “deprimido” se não conseguir chegar a 100% de cobertura – a maioria dos desenvolvedores raramente chega a este valor. Nosso objetivo final sempre será desenvolver
aplicações sem bugs que agradam imensamente nossos usuários. Neste sentido, as ferramentas de análise de cobertura serão de grande valia, ajudando você a ser visto por todos como um ótimo profissional. Entenda que desenvolver, usar testes, ferramentas de cobertura e preocupar-se com a qualidade e manutenibidade de seus testes não é perda de tempo. Todo o tempo gasto com este tipo de preocupação é devidamente recuperado com a diminuição das necessidades de manutenção e com o aumento da qualidade do seu produto final, diminuindo o down time dos aplicativos que você coloca no mercado e ampliando a sua fama de bom profissional.
/para saber mais Na MundoJava 41 tivemos um ótimo artigo sobre testes e desenvolvimento baseado em testes (TDD), que cobre alguns dos principais aspectos destes conceitos que podem lhe ajudar bastante a desenvolver aplicativos com menos erros. Na MundoJ 44 eu escrevi um artigo sobre os princípios da programação sem bugs que buscava explicar os princípios básicos que devem nortear a boa programação e o desenvolvimento de testes unitários para garantir o funcionamento correto de nossos programas. Na MundoJ 46 existe um artigo muito interessante sobre a qualidade através de testes funcionais usando três ferramentas específicas. Mesmo que você não use as ferramentas em questão, os conceitos passados pelo artigo poderão ser de grande valia.
/referências > BECK, K.; BEEDLE, M., 2001, “Manifesto for Agile Software Management”, site da Internet de endereço http:// agilemanifesto.org/, última visita em setembro/2011 > FOWLER, M.; BECK, K. et al, 1999, “Refactoring: Improving the desing of existing code”, Addison-Wesley Professional, EUA > HUNT, A.; THOMAS, D., 2004, “Pragmatic Unit Testing with Junit”, The Pragmatic Programmers, EUA > MARTIN, R., 2008, “Clean Code – A Handbook of Agile Software Craftsmanship”, Prentice Hall, Nova Iorque, EUA > SANCHEZ, I., 2006, “Confusões sobre TDD”, site da internet de endereço http://dojofloripa.wordpress. com/2006/11/28/confusoes-sobre-tdd/, última visita em setembro/2011
35 \
grafos_
Visualização Gráfica de Grafos com a API JUNG Grafos podem ser usados para representar relacionamentos em diversos tipos de redes: sociais, de computadores, de tráfego e outros tipos de dados relacionais. Frequentemente é necessário representar grafos de forma visual para ilustrações ou análises. Neste artigo, veremos como usar a API JUNG para visualizar conjuntos de dados representados como grafos. Este artigo complementa o artigo “Introdução à Representação e Análise de Grafos com a API JUNG”, publicado na Edição 49 da revista MundoJ.
Grafos são estruturas que servem para representar muitos objetos reais de forma natural: redes sociais e relações entre objetos em geral, redes de computadores e estradas, hierarquias de dados em vários tipos de sistema e muitos outros conceitos podem ser representados como grafos. Em um artigo na última edição vimos como representar grafos e executar análises simples em Java usando a API JUNG (Java Universal Network/Graph Framework). Neste artigo, veremos como usar a mesma API para visualizar estes grafos.
G
rafos são estruturas de dados que representam objetos e relações entre eles. Grafos podem ser usados para representar vários tipos de conceitos e objetos do mundo real, como, por exemplo, relações em redes sociais reais ou virtuais, links em documentos na Web, rotas de tráfego de veículos ou de redes de computadores e muitos outros. Grafos são conjuntos de vértices (que correspondem aos objetos que queremos representar) e arestas, que ligam pares de vértices e correspondem às relações entre os objetos. No artigo “Introdução à Representação e Análise de Grafos com a API JuNG”, publicado na Edição 49 da revista MundoJ, apresentamos a API JuNG (Java universal Network/Graph Framework), que permite a representação de grafos em Java como coleções de vértices e arestas. Além de classes para representar grafos de diversos tipos, a API contém implementações de vários algoritmos para análise dos grafos e operações como criação de subconjuntos dos grafos. Neste artigo veremos outras classes da API que permitem a visualização dos grafos usando também diversos algoritmos para determinação das posições
/ 36
e aparência de seus vértices e arestas. visualização de grafos pode ser usada para ilustração simples: é mais fácil entender os objetos e suas relações em um grafo quando este é representado graficamente do que pela lista de vértices e arestas. visualização também pode ser usada para análises visuais mais complexas: alguns fenômenos podem ser mais bem compreendidos ou identificados através da visualização dos grafos, em especial quando associamos outras informações sobre vértices e arestas à representação visual dos grafos. visualização de grafos é uma tarefa mais complexa do que aparenta ser: grafos com grande número de arestas e vértices podem ter uma representação visual muito densa e até incompreensível. Adicionalmente não existe uma única forma de visualizar um grafo – os vértices podem ser posicionados de muitas formas diferentes, o que afeta também a aparência das arestas que conectam estes vértices. Embora alguns tipos específicos de grafos possam ser visualizados de forma natural (árvores, por exemplo), muitos tipos de grafos podem ser representados graficamente de forma diferente, mesmo tendo a
Rafael Santos | rafael.santos@lac.inpe.br é doutor em Inteligência Artificial pelo Instituto Tecnológico de Kyushu, Japão. É tecnologista do Instituto Nacional de Pesquisas Espaciais, atuando em pesquisa e desenvolvimento de aplicações e técnicas de mineração de dados, processamento de imagens e inteligência artificial aplicada, e presentemente é pesquisador visitante da Universidade Johns Hopkins, nos Estados Unidos. É autor do livro “Introdução à Programação Orientada a Objetos usando Java” e de várias palestras e tutoriais sobre Java.
André Grégio | argregio@cti.gov.br é mestre em Computação Aplicada pelo Instituto Nacional de Pesquisas Espaciais. É tecnologista do Centro de Tecnologia da Informação Renato Archer (CTI), atuando em pesquisa e desenvolvimento na área de segurança de sistemas de informação.
mesma representação como conjuntos de vértices e arestas. Isto é exemplificado na figura 1, que mostra quatro layouts ou representações gráficas diferentes para o mesmo grafo (mesmo conjunto de vértices com o mesmo conjunto de arestas conectando estes vértices). Este artigo apresenta seções mostrando como criar aplicações para visualização básica de grafos com a API JuNG, comenta sobre os layouts disponíveis na API (que determinam como os vértices de um grafo serão posicionados na visualização), e apresenta diversos exemplos de como a aparência gráfica de vértices e arestas podem ser modificados para visualização do grafo.
Para visualizar um grafo representado na API JuNG precisamos primeiro de uma instância de classe que implementa a interface Graph. Os passos para a criação de uma representação visual do grafo são: 1. usando a instância que representa o grafo, criamos um layout para a representação gráfica deste. Layouts em JuNG são implementados por instâncias de classes que herdam de AbstractLayout (que por sua vez implementa a interface Layout) e que devem ser declaradas com os mesmos parâmetros de tipo usa-
Visualização básica de grafos com a API JUNG
A API JuNG permite a representação e processamento de grafos em Java de forma eficiente e flexível. Os arquivos da API podem ser copiados de http://sourceforge.net/projects/jung/files/. A versão mais recente em setembro de 2011 é a 2.0.1. A API é distribuída como um arquivo .zip, que contém 17 arquivos .jar, que devem ser adicionados ao classpath da máquina virtual Java ou do projeto que usa a API. Para adicionar estes arquivos .jar em um projeto desenvolvido com a IDE Eclipse, clique no nome do projeto com o botão direito do mouse, selecionando o menu Build Path / Add External Archives e escolhendo os nomes dos arquivo .jar no diálogo de seleção. Estas instruções são as mesmas apresentadas no artigo complementar na Edição 49.
A API JuNG contém vários métodos que facilitam a visualização de grafos de diversos tipos, permitindo também a definição da aparência gráfica de vértices e arestas de forma bastante flexível, o layout (distribuição) dos vértices e arestas usando vários algoritmos diferentes ou mesmo de forma manual e algumas formas de interação do usuário com a representação visual do grafo. A API JuNG foi criada de forma a facilitar bastante a criação de aplicações para visualização de grafos, usando algumas classes básicas para as tarefas mais comuns e permitindo a customização de vários aspectos através da criação de instâncias de classes específicas com tarefas bem definidas.
Instalando a API JUNG
Figura 1. Quatro representações gráficas do mesmo grafo. 37 \
dos para declarar a instância que representa o grafo. 2. usando a instância que representa o layout, criamos uma instância de visualizationviewer, classe que herda indiretamente de JComponent e que será usada como componente gráfico na criação da interface gráfica para visualização. 3. Opcionalmente recuperamos o método getRenderContext() da instância de visualizationviewer para acessar o objeto correspondente ao contexto de renderização. Este objeto possui métodos para definir, através de classes transformadoras, a aparência dos vértices e arestas no grafo. 4. Opcionalmente associamos comportamentos ao componente de visualização para responder a gestos do mouse – isto é usado para permitir transformações como pan e zooming no grafo e seleção e modificação das arestas, entre outras funções. 5. usamos a instância de visualizationviewer para compor a interface gráfica da aplicação. Para ilustrar estes passos vejamos um exemplo simples baseado em um grafo apresentado no artigo “Introdução à Representação e Análise de Grafos com a API JuNG”, publicado na Edição 49. Para facilitar a leitura, o trecho de código que cria este grafo é mostrado na Listagem 1. O grafo criado tem vértices e arestas do tipo String, e é o mesmo grafo usado para criar as visualizações mostradas na figura 1.
Listagem 1. Trecho de código que cria um grafo no qual vértices e arestas são Strings. // Este método cria um grafo simples para uso em alguns exemplos deste artigo. public static Graph<String,String> criaGrafo() { //Criamos um grafo onde vértices e arestas são Strings Graph<String,String> grafo = new UndirectedSparseGraph<String,String>(); // Criamos as arestas e vértices simultaneamente. grafo.addEdge(“A-B”,”A”,”B”); grafo.addEdge(“A-C”,”A”,”C”); grafo.addEdge(“A-M”,”A”,”M”); grafo.addEdge( “B-C”,”B”,”C”); grafo.addEdge(“B-D”,”B”,”D”); grafo.addEdge( “B-J”,”B”,”J”); grafo.addEdge(“B-K”,”B”,”K”); grafo.addEdge( “B-L”,”B”,”L”); grafo.addEdge(“C-D”,”C”,”D”); grafo.addEdge( “C-E”,”C”,”E”); grafo.addEdge(“D-F”,”D”,”F”); grafo.addEdge( “D-G”,”D”,”G”); grafo.addEdge(“D-H”,”D”,”H”); grafo.addEdge( “D-I”,”D”,”I”); grafo.addEdge(“D-J”,”D”,”J”); grafo.addEdge( “E-N”,”E”,”N”); grafo.addEdge(“F-G”,”F”,”G”); grafo.addEdge(“J-K”,”J”,”K”); grafo.addEdge(“L-M”,”L”,”M”); return grafo; } / 38
Os passos para a criação da visualização do grafo são mostrados na Listagem 2 (para manter as listagens concisas, os passos opcionais, usados para definir diversos aspectos visuais do grafo, não são implementados nesta versão do código. A visualização criada pela Listagem 2 pode ser vista na figura 2).
Listagem 2. Primeira versão da classe para visualização do grafo criado na Listagem 1. // Classe para criar uma visualização básica de um grafo. public class VisBasica { public VisBasica() { // O grafo é criado em outra classe, para organizar // melhor o código. Graph<String,String> grafo = CriaGrafo1.criaGrafo(); // Passo 1: criamos uma instância de classe para // controlar o layout. AbstractLayout<String,String> layout = new KKLayout<String,String>(grafo); // Passo 2: criamos o componente para visualização. VisualizationViewer<String,String> componente = new VisualizationViewer<String,String>(layout); //Modificamos características do componente componente.setPreferredSize( new Dimension(750,750)); componente.setBackground(Color.WHITE); //Passo 5: usamos o componente para montar a GUI JFrame f = new JFrame(“Visualização de Grafos”); f.getContentPane().add(componente); f.pack(); f.setVisible(true); f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); } // Executa a classe como uma aplicação. public static void main(String[] args) { new VisBasica(); // Monta a GUI. } }
Figura 2. Visualização criada pelo código na Listagem 2.
A figura 2 mostra uma visualização bastante básica do grafo criado na Listagem 1 – alguns passos
que permitem a customização da aparência dos vértices e arestas não foram executados, para manter a primeira versão do código simples. Podemos observar que as arestas e vértices da figura 2 estão em posições diferentes das arestas e vértices mostradas nas diversas visualizações da figura 1, embora o grafo usado nas figuras seja o mesmo. Existem duas razões para estas diferenças: a primeira é porque escolhemos classes diferentes para representar e definir o layout que será usado para visualização do grafo; a segunda é que alguns dos layouts implementados na API JuNG dependem de inicialização das posições das arestas, que pode ser aleatória em alguns casos. Em outras palavras, mesmo usando a mesma classe para implementar o layout podemos ter diferenças na aparência final da visualização (basta executar a classe mostrada na Listagem 2 algumas vezes para ver a variação dos resultados). Existem soluções na própria API para resolver este problema, que serão vistas em outras seções neste artigo.
Alguns layouts para visualização de grafos
Ao criar visualizações de grafos é importante considerar que tipo de layout deve ser usado, e que alterações nas aparências das arestas e vértices devem ser feitas para que o resultado seja o mais próximo do esperado. A definição do tipo de layout é simples para alguns tipos de grafos, como árvores e florestas ou organogramas e fluxogramas, mas não é trivial para outros tipos de grafos, em especial os genéricos. Nestes casos, sugere-se tentar diferentes layouts, verificando que implementação da API produz os resultados mais agradáveis ou interpretáveis. Alguns layouts implementados na API JuNG e suas respectivas classes são listados a seguir. Detalhes específicos das classes não serão apresentados: o leitor pode obter mais informações (inclusive referências bibliográficas sobre os algoritmos de layout correspondentes às classes) na documentação da API. » CircleLayout: faz com que os vértices sejam distribuídos em um círculo, com distância igual entre eles. A quarta visualização mostrada na figura 1 foi criada usando uma instância desta classe. Não realiza cálculos interativos para determinar a posição final do vértice, e não usa posicionamento aleatório inicial. O raio do círculo é determinado automaticamente, mas pode ser definido pelo usuário através do método setRadius. » FRLayout: usa o algoritmo iterativo Fruchterman-Reingold para determinar as posições finais dos vértices através de
»
»
»
»
»
»
cálculos de atração e repulsão entre vértices. O usuário pode mudar o comportamento do algoritmo modificando as constantes de atração e repulsão e o número máximo de iterações. A API JuNG possui também a classe FRLayout2, que contém uma versão melhorada, mas ainda experimental do algoritmo. KKLayout: usa o algoritmo iterativo Kamada-Kawai para determinar as posições finais dos vértices através de uma heurística que considera os comprimentos dos vértices que conectam as arestas do grafo. SpringLayout: usa um método iterativo para posicionar os vértices, que tenta aproximar e afastar grupos de arestas considerando os vértices que as unem. A execução do algoritmo que calcula o layout das arestas é consideravelmente lenta, sendo possível ver a sua modificação durante a exibição do componente de visualização. ISOMLayout: inicializa as coordenadas dos vértices de forma aleatória e usa uma variante do Mapa Auto-Organizável de Kohonen (um tipo de rede neural) para calcular as posições finais dos vértices. Embora sua execução seja iterativa e lenta este layout pode ser adequado para visualização de grafos esparsos. TreeLayout: usado para visualizar florestas ou conjuntos de árvores (não pode ser usado diretamente para visualizar grafos como os usados nos exemplos deste artigo). Modifica o posicionamento inicial dos vértices para espalhar as árvores da floresta na área do componente gráfico. RadialTreeLayout: similar ao TreeLayout, tenta organizar as árvores da floresta em círculos concêntricos, com as raízes das árvores no centro do componente. StaticLayout: permite ao programador determinar posições específicas para cada vértice. Este layout garante consistência entre execuções de uma aplicação: os vértices estarão sempre na posição definida pelo programador. É possível usar uma função específica para determinar a posição de cada vértice, ou explicitar as coordenadas de cada vértice, mas isto pode ser impraticável para grandes grafos. um exemplo de uso desta classe será apresentado neste artigo.
39 \
}
public Stroke transform(Trecho t) { float largura = 1f; if (caminho.contains(t)) largura = 3f; if (t.isPassaÔnibus()) return new BasicStroke(largura, BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0,new float[]{5},0); else return new BasicStroke(largura); }
A classe vMv_EdgeDrawT, mostrada na Listagem 24, determina a cor que será usada para desenhar as arestas. A mesma classe será usada para determinar a cor das arestas, e a cor e preenchimento das setas usadas nas arestas (veja a classe visMv na Listagem 13).
dalGraphMouse (com parâmetros de tipo iguais ao do grafo). Depois basta criar uma instância de GraphZoomScrollPane recebendo a instância de visualizationviewer como parâmetro para seu construtor. O componente automaticamente processará os seguintes gestos do mouse: » Clicar e arrastar o mouse: modifica a área de visualização do grafo no componente (pan). » usar o mouse wheel: amplia ou reduz a área visualizada (zoom). » Clicar e arrastar o mouse pressionando shift: gira o grafo no componente. » Clicar e arrastar o mouse pressionando control ou command: deforma o grafo no componente na vertical ou horizontal, dependendo da direção de arrasto (skew). »
Listagem 24. Classe VMV_EdgeDrawT, que determina a aparência do traço usado para desenhar os vértices. // Esta classe transforma uma instância de Trecho em // uma instância de Paint que será usada para desenhar // a aresta. Arestas no caminho mínimo serão // desenhadas em azul, as outras em preto. public class VMV_EdgeDrawT extends VMV_TransBase implements Transformer<Trecho,Paint> { public VMV_EdgeDrawT(Graph<Ponto,Trecho> grafo, List<Trecho> caminho) { super(grafo,caminho); } public Paint transform(Trecho t) { if (caminho.contains(t)) return Color.BLUE; else return Color.GRAY; } }
Figura 5. Interface gráfica criada pela classe VisVM (Listagem 12).
As figuras 5, 6 e 7 mostram a interface gráfica criada pela classe visMv (Listagem 13). A figura 5 mostra a aparência inicial, a figura 6 mostra o grafo reduzido para poder ser visualizado por inteiro e a figura 7 mostra um trecho do grafo ampliado – é possível observar que os desenhos têm características vetoriais: a ampliação não degrada a qualidade das linhas e elementos gráficos. A classe GraphZoomScrollPane, parte da API JuNG, facilita bastante a visualização de grandes grafos: ela funciona como uma versão de JScrollPane, e contém métodos para processar a interação do mouse com o componente. Para usar esta classe é preciso primeiro definir o comportamento do mouse para o componente de visualização (neste exemplo, instância de visualizationviewer) através da chamada ao método setGraphMouse, passando como argumento a classe que Figura 6. Interface gráfica criada pela classe VisVM (mostrando determina o comportamento, neste caso, DefaultMotodo o grafo).
49 \
/para saber mais > O artigo “Introdução à Representação e Análise de Grafos com a API JUNG”, publicado na Edição 49 da revista MundoJ, mostra como representar e processar grafos de diversos tipos, inclusive como executar algoritmos de análise sobre os grafos. > O site da API JUNG contém vários exemplos e acesso à documentação da própria API gerada pela ferramenta javadoc, e pode ser acessado em http://jung.sourceforge. net/doc/index.html. Um tutorial curto, mas simples da API JUNG pode ser encontrado em http://www.grottonetworking.com/JUNG/JUNG2-Tutorial.pdf (ambos em inglês). > O documento http://jung.sourceforge.net/doc/ JUNGVisualizationGuide.html (também em inglês) explica os conceitos usados no design das classes da API para visualização, e foi usado como base para parte deste artigo. > Conceitos básicos de grafos podem ser encontrados em diversas páginas na Wikipédia, em especial em http://pt.wikipedia.org/wiki/Teoria_dos_grafos e http:// pt.wikipedia.org/wiki/Grafo. Alguns conceitos e referências sobre visualização de grafos, particularmente sobre os layouts, pode ser vista em http://en.wikipedia.org/wiki/ Graph_drawing (em inglês).
VisualizationViewer<Ponto,Trecho> componente = new VisualizationViewer<Ponto,Trecho>(layout); DefaultModalGraphMouse<Ponto,Trecho> graphMouse = new DefaultModalGraphMouse<Ponto,Trecho>(); graphMouse.setMode(ModalGraphMouse.Mode.PICKING); componente.setGraphMouse(graphMouse);
Considerações finais Figura 7. Interface gráfica criada pela classe VisVM (mostrando região ampliada).
A instância da classe DefaultModalGraphMouse que é usada para controlar o comportamento do mouse para o componente de visualização também permite operações de seleção de vértices: basta criar a instância desta classe e executar o método setMode(ModalGraphMouse.Mode.PICKING) antes de associar o comportamento com a classe de visualização. vértices podem ser selecionados com o mouse e arrastados para novas posições. O trecho de código apresentado na Listagem 25 mostra como isto pode ser feito.
Listagem 25. Trecho de código que demonstra como
configurar o componente de visualização para permitir seleção de vértices no grafo.
/ 50
A API JuNG contém classes para facilitar a visualização de grafos de diversos tipos, com várias formas de alterar a aparência visual dos elementos do grafo. A API contém também algumas classes que implementam algoritmos de layout dos vértices e arestas de um grafo, mas também permite a determinação manual do layout. A criação de exemplos de visualização requer a criação de várias classes que determinam a aparência de vários elementos gráficos do grafo, o que é complexo e trabalhoso. Se o objetivo é criar uma ilustração simples de um grafo pode ser mais simples usar de um editor gráfico; por outro lado a abordagem programática permite a automação da tarefa de desenho com grande flexibilidade, como demonstrado – no exemplo do grafo de malha viária, para visualizar o caminho mais curto entre outros pontos basta mudar uma linha de código.
Assine a melhor revista de desenvolvimento de software
para ler artigos na revista virtual
acesse www.revistamundoj.com.br ou ligue para (41) 3029-9353
51 \
coluna_
tendências em foco
Conhecendo o Hadoop Volta e meia em eventos sobre Cloud Computing surge o termo Hadoop. Já ouvi em uma palestra que o grande desafio de desenvolver aplicações para Cloud era exatamente a complexidade no uso do Hadoop. O que gera a confusão é que o Hadoop e o MapReduce, do qual ele se originou, vêm sendo usados pelas empresas de Internet, que inspiraram o modelo de Cloud Computing, e que precisam de escala massiva para suas aplicações, como Yahoo, Google e Facebook. Mas dizer que o Hadoop é a base para todo e qualquer projeto de Cloud absolutamente não é correto. O Hadoop é usado para aplicações analíticas de dados massivos, que não é o dia-a-dia das empresas que usam ou pretendem usar Cloud Computing.
O
Hadoop foi criado pelo Yahoo em 2005 e pode ser considerado uma das maiores invenções de data management desde o modelo relacional. Hoje é um dos projetos da comunidade Apache e vem sendo adotado por empresas que precisam tratar volumes massivos de dados não estruturados. Já existe inclusive um ecossistema ao seu redor, mas ainda vai demandar algum tempo para se disseminar de forma mais ampla pelo mercado. Neste post vamos debater um pouco mais o que é e o que não é o Hadoop, seu mercado e tentar visualizar algumas tendências. Quem sabe acertamos algumas? Mas, o que é o Hadoop? É, na prática, uma combinação de dois projetos separados, que são o Hadoop MapReduce (HMR), que é um framework para processamento paralelo e o Hadoop Distributed File System (HDFS). O HMR é um spinoff do MapReduce,
/ 52
software que o Google usa para acelerar as pesquisas endereçadas ao seu buscador. O HDFS é um sistema de arquivos distribuídos otimizados para atuar em dados não estruturados e é tambem baseado na tecnologia do Google, neste caso, o Google File System. Existe também o Hadoop Common, conjunto de bibliotecas e utilitários que suportam os projetos Hadoop. Na prática, para que o HMR processe os dados, eles devem estar armazenados no HDFS. O Hadoop é um projeto Open Source, com licenciamento Apache e, portanto, permite a criação de um ecossistema de negócios baseados em distribuições específicas. E o surgimento de serviços em nuvem, como o Amazon Elastic MapReduce, permite às empresas tratarem dados massivos sem demandar aquisição de servidores físicos. Neste modelo, o usuário escreve a aplicação Hadoop e a roda em cima da nuvem
da Amazon. A base das distribuições Hadoop é a comunidade Apache. Diversas empresas vêm contribuindo com código para seu desenvolvimento como a Yahoo, Facebook, Cloudera, IBM e outras. Em torno do código-base, surgem as distribuições, como Cloudera (www.cloudera.com) e DataStax (http://www.datastax.com/brisk), que agregam valor com utilitários e serviços de suporte e educação, no mesmo modelo das distribuições Linux. Interessante que a distribuição da DataStax, chamado de Brisk, substituiu o HDFS por um sistema de arquivos distribuídos baseados no software NoSQL Cassandra, chamado agora de CassandraFS. Mas, em torno do Hadoop (http://hadoop.apache.org/), a comunidade Apache mantém diversos projetos relacionados, como o Hbase, que é um banco de dados NoSQL que trabalha em cima
do HDFS. Este banco de dados é usado pelo Facebook para suportar seu sistema de mensagens e os seus serviços de informações analíticas em tempo real. Existe também o Hive, criado pelo Facebook, que é uma camada de data warehouse que roda em cima do Hadoop. utiliza uma linguagem chamada Hive SQL, similar à SQL, o que facilita sua utilização, pois desenvolvedores acostumados com SQL não encontram maiores dificuldades em trabalhar com o Hive SQL. Outro e também muito interessante projeto é o Pig, criado pelo Yahoo. É uma plataforma que permite análises de arquivos muito grandes usando uma linguagem de alto nível chamada de Pig Latin. Olhando-se o stack de softwares do Hadoop, o Pig se situa entre o Hive e o HMR e é uma tentativa de fornecer uma linguagem de alto nivel para se trabalhar com o Hadoop. Outros projetos menos conhecidos são o Avro (sistema de serialização de dados), o Chukwa (monitoramento de sistemas distribuídos) e o Hama (para computações científicas massivas). A IBM usa intensamente o Hadoop em diversos projetos, o integrando com outros de seus softwares como o Cognos, criando soluções para tratamento analítico de dados massivos e não estruturados, como o InfoSphere
BigInsights, que agrega um conjunto de tecnologias open source como o próprio Hadoop, Nutch e Pig, com as tecnologias próprias da IBM, como InfoSphere e ManyEyes. vejam em (http:// www-01.ibm.com/software/data/ bigdata/). A IBM também desenvolveu uma variante do HDFS chamado de IBM General Parallel File System (GPFS), que pode ser visto em http://www-03.ibm.com/ systems/software/gpfs/. Ok, e quem usa Hadoop? Existem os casos emblemáticos como Facebook, Yahoo, Twitter e Netflix (na nuvem da Amazon), mas também já começamos a ver seu uso em ambientes corporativos brick-and-mortar. Recentemente uma pesquisa mostrou que pelo menos umas 20 empresas da lista da Fortune mil assumiram publicamente que usam Hadoop de alguma forma. A adoção do Hadoop em aplicações analíticas corporativas como as ofertadas pela IBM vão ajudar na sua disseminação. Só para lembrar, quando a IBM anunciou seu apoio ao Linux, em 2001, o Linux passou a ser visto sob outra ótica pelo ambiente corporativo. Nem todo usuário de Hadoop demanda uma escala massiva de dados ao nível do Facebook ou Yahoo. Mas empresas com razoável volume de informações não estruturadas, como bancos, varejo, empresas aéreas e outras
vão encontrar no Hadoop uma boa alternativa para o tratamento analítico dos seus dados. vejam alguns exemplos em ftp://public. dhe.ibm.com/common/ssi/ecm/en/ imb14103usen/IMB14103USEN. PDF. O Hadoop ainda está nos primeiros passos de sua evolução e disseminação pelo mercado. Mas tenho certeza de que em poucos anos ele será bem mais conhecido e utilizado. uma pesquisa pelo termo Hadoop no Google Trends já aponta um crescimento significativo no interesse pela tecnologia, como podemos ver em http://www.google.com/ trends?q=hadoop. Na minha opinião vale a pena investir tempo estudando a tecnologia. um dos problemas com o Hadoop é realmente a sua complexidade de uso, demandando desenvolvedores altamente qualificados. A sua curva de aprendizado é bastante íngreme e praticamente inexiste gente habilitada para usar o Hadoop adequadamente. Bem, temos aí uma boa oportunidade para as universidades inserirem seu estudo e prática em seus cursos de Ciência da Computação. uma sugestão para quem quer estudar Hadoop mais a fundo: acessem www.ibm.com/developerworks e pesquisem pela keyword Hadoop. vocês vão encontrar milhares de artigos muito interessantes.
Cezar Taurion | ctaurion@br.ibm.com |www.ibm.com/developerworks/blogs/page/ctaurion Formado em Economia e Ciências da Computação, atualmente gerente de Novas Tecnologias Aplicadas da IBM Brasil. Também pode ser encontrado no Twitter, em @ctaurion.
53 \
JPA_
Comparando Persistência de Dados com JPA
TOPLINK E HIBERNATE TESTADOS EM DIFERENTES BANCOS DE DADOS E SISTEMAS OPERACIONAIS.
Este artigo descreve os testes com diversas combinações de frameworks, banco de dados e sistemas operacionais analisando qual o melhor na avaliação de desempenho no que diz respeito à persistência de dados em suas versões padrão, sem qualquer tunning. Abordaremos MySQL, PostgreSql, Oracle, SQL Server, Windows, Ubuntu, Toplink e Hibernate.
A
persistência é uma das áreas de maior importância em uma aplicação JEE (Java Enterprise Edition), sendo especificada pela JPA (Java Persistence API), que originalmente fazia parte da plataforma EJB (Enterprise Java Beans). A partir da versão 5 do Java a JPA passou a ter a sua própria especificação, JPA 1.0. Com o uso de frameworks para persistência o desenvolvedor obtém um nível mais alto de abstração sobre o tradicional JDBC, permitindo utilizar o mecanismo de ORM (Object to Relational Management) em sua aplicação, diminuindo o acoplamento da tecnologia do SGBD (Sistema Gerenciador de Banco de Dados) ao código orientado a objetos. O acoplamento reduzido amplia a possibilidade de seleção de tecnologia na camada chamada de EIS (Enterprise Information System) que, segundo a documentação JEE 6, permite a integração entre diferentes plataformas e mecanismos de armazenamento de dados. Para este trabalho, as tecnologias selecionadas foram MySQL, PostgreSql, Oracle e SQLServer. Neste artigo, serão demonstrados os resultados relacionados ao desempenho entre dois frameworks baseados em JPA: Hibernate e TopLink. Foram ana-
/ 54
lisadas diferentes combinações dos frameworks com os sistemas operacionais Windows 2003 Server e Linux ubuntu 11.04, junto aos bancos de dados citados. Os sistemas operacionais foram selecionados buscando abranger o mercado brasileiro de softwares licenciados e opções de softwares livres mais populares.
Persistência de dados
A persistência de dados nos possibilita trabalhar com o ORM, que permite a criação de classes Java como beans de entidade, mapeando-as para tabelas em um banco de dados relacional. uma das vantagens do uso da JPA é a não obrigatoriedade do uso de instruções SQL, o que aumenta a velocidade de desenvolvimento do software e não restringe as instruções de persistência a um padrão ou versão específica de SQL. A criação das instruções SQL é de responsabilidade do framework provedor de JPA sendo utilizado na camada de persistência. Desta forma, um software que utiliza JPA torna-se não dependente das particularidades da linguagem SQL própria do banco de dados, o que é muito útil, uma vez que o desenvolvedor não precisa reescrever todo o código adaptando-
Sylvio Barbon Junior | sbarbonjr@gmail.com é formado em Engenharia e Ciência da Computação, mestre e doutor em Física Computacional pelo IFSC/USP. Realiza trabalhos como consultor Java no interior do Estado de São Paulo. Possui certificação SCJP, é docente na Universidade do Estado de Minas Gerais (UEMG) e na FATEC de São José do Rio Preto. Junto com outros colaboradores mantém o site http://www.patternizando.com. br para a discussão de desenvolvimento de software.
Marcos Pedro Gomes da Silva | marcos@marcpedro.com atualmente é graduando em Sistemas de Informação pela Universidade do Estado de Minas Gerais (UEMG) entusiasta da linguagem Java e empresário.
-o para outro banco de dados, aumentando a compatibilidade e portabilidade do software, desfrutando da manipulação baseada em objetos.
O Hibernate
Mantido pela JBoss, foi criado em 2001 por Gavin King e é um framework livre e de código aberto licenciado sob a LGPL v2.1, segue o padrão JPA e disponibiliza uma versão para a tecnologia .Net. Atualmente está em sua versão 3.6.6 (estável) e contém uma vasta documentação disponível no site na mantenedora.
O TopLink
Criado pela Oracle, teve sua primeira distribuição no início da década de 90 e é um framework proprietário, também segue o padrão JPA e atualmente está na versão 11g.
Ambiente de simulação
Todos os testes foram executados em notebook Sony vaio com processador Core 2 Duo 2.0GHz com 4GB de memória RAM. O software utilizado para os testes controlava o tempo de cada tarefa para posterior comparação, considerando ainda a possibilidade de testar em diferentes Sistemas Operacionais. Além disso, em todos os casos, a mesma máquina virtual foi utilizada, neste caso a JvM 1.6.
Figura 1. Diagrama do Banco de Dados dos Testes.
O intuito deste artigo não é definir qual é a melhor das tecnologias, mas sim, analisar apenas o seu desempenho sem nenhuma personalização em sua configuração, preservando-a como foi lançada ao mercado. Sabe-se que com o uso de personalizações e configurações aprimoradas (tunning), assim como o uso de pools de conexão, o resultado pode ser diferente do encontrado neste artigo. Foram utilizadas as seguintes tecnologias: Produtos e versões » Frameworks persistência: Hibernate versão 3.2.5, compatível com JPA 1.0 TopLink versão 2.1 build 3, compatível com JPA 1.0 » Bancos de dados: MySql versão 5.1.11 PostgreSql versão 8.4.4 Oracle versão 10g Express Edition – 10.2.0 SqlServer 2005 versão 9.00.4053.00 Express » Sistemas operacionais: Windows 2003 Service Pack 2 Linux ubuntu 11.04, kernel 2.6.35-24 » Java: Máquina virtual Java versão 1.6.0_22 Para a simulação de um cenário de software real, foi criada uma estrutura com 10 entidades relacionadas. Cada entidade apresentava atributos de tipos variados, porém presentes em todos os sistemas gerenciadores de bancos de dados testados. Os tipos de dados foram INT, DATE, TExT e FLOAT, como mostra a figura 1. Outra característica utilizada para simular situações reais foi o relacionamento entre as entidades, em que foram utilizados relacionamentos diversos, com cenários de persistência em cascata e exportação de chaves estrangeiras que exigem a geração de sintaxes mais complexas pelo framework JPA. Os dados foram gerados aleatoriamente pelo sistema, respeitando o tipo de dado de cada atributo e utilizando o limite máximo de informação de cada tipo de dado. A Listagem 1 apresenta um exemplo de entidade mapeada para posterior manipulação pelos frameworks. Considerando que os frameworks são selecionados pela configuração no persistence.xml,
55 \
para cada opção é criada uma nova unidade de persistência (Persistence unit), o que permite o mesmo mapeamento para qualquer framework que segue o padrão JPA, com mínimas modificações apenas no persistence.xml.
Listagem 1. Implementação da entidade Table3. @Entity @Table(name = “table3”) public class Table3 implements Serializable { @Id @SequenceGenerator(name = “seq_table3”, sequenceName = “seq_table3”, allocationSize = 1) @GeneratedValue(generator = “seq_table3”, strategy=GenerationType.AUTO) @Column(name = “idtable3”) private int idTable3;
Tabela 1. Tempo total da comparação entre TopLink e Hibernate.
Framework
Tempo Total Médio
Hibernate TopLink
0h48m46s 1h29m18s
Comparando os dois frameworks, o Hibernate obteve o melhor desempenho, mostrando-se ser mais rápido, realizando todas as operações em menos de uma hora como demonstrado pelo gráfico da figura 2.
Desempenho entre sistemas operacionais
Nestas simulações foram executados 96 testes em cada sistema operacional, desprezando o SQL Server no ambiente Windows, para que a quantidade de simulações fosse equivalente, já que ele não é compatível em ambiente Linux. Na tabela 2 são apresentados os resultados obtidos, que corresponde à média entre os dois frameworks no respectivo sistema operacional utilizando todos os bancos testados, com exceção do SQL Server. A quantidade de registros manipulados foi de 669.900.
@Column(name = “valor1”) private int valor1; @Column(name = “valor2”) private float valor2; @Column(name = “texto”, length=1000) private String texto; @ManyToOne @ForeignKey(name=”fk_table3_table2”) @JoinColumn(name = “idtable2”, referencedColumnName = “idtable2”, nullable = false) private Table2 table2; }
média independentemente de banco de dados e sistema operacional. Ao total foram manipulados 781.200 registros para cada teste.
Tabela 2. Tempo total da comparação entre sistemas operacionais.
Sistema Operacional Linux Windows
// Gets e Sets
Tempo Total Médio 0h49m41s 1h08m45s
Nesta comparação, como exibido na figura 3, o sistema operacional Linux obteve tempo menor, em Desempenho entre Hibernate e TopLink que ao final de todas as operações o tempo foi inferior Foram executados 112 testes em cada framework à uma hora. nos dois sistemas operacionais, quatro bancos de dados variando entre 1, 2, 3 e 4 tabelas (mostradas na Desempenho entre banco de dados Nestas simulações, foram executadas 32 operafigura 1) em cada banco de dados (com exceção do SQL Server no ubuntu), e todo o tempo gasto foi so- ções para cada banco de dados, sendo elas: inserção, mado e o resultado está exibido na figura 2. A tabela seleção, atualização e exclusão de dados, variando 1 mostra o resultado de tempo total em horas, que é a entre 1, 2, 3, 4 tabelas. Os resultados dos testes em
Tempo gasto entre frameworks
Tempo gasto entre sistemas operacionais 4,5
Tempo 4
Tempo 6
(em milhões de 3,5 milissegundos) 3
(em milhões de 5 milissegundos)
4
Hibernate TopLink
3 2 1 0
Frameworks Figura 2. Gráfico de comparação entre frameworks. / 56
2,5 2 1,5 1 0,5 0
Linux Windows
Figura 3. Gráfico de comparação entre sistemas operacionais.
referências/ > Keith, Mike; Shincariol, Merrick. EJB 3 Profissional: Java Persistence API. Rio de Janeiro: Moderna, 2008 > Burke, Bill; Monson-Haefel, Richard, Enterprise JavaBeans 3.0, São Paulo, 2007 > Bauer, Christian; King, Gaving, Hibernate in Action, Manning, 2004 > Documentação Oficial Hibernate, disponível em <www. hibernate.org> Acessado em 12 de julho de 2011 > Documentação oficial TopLink, disponível em <http:// www.oracle.com/technetwork/middleware/toplink / overview/index.html> Acessado em 15 de julho de 2011
Tabela 4. Resultados em ambiente Linux. Banco de dados Tempo total médio PostgreSQL Oracle MySql
0h13m35s 0h13m45s 0h22m21s
Em ambiente Windows, o SQL Server mostrou-se ser mais rápido perante os outros. Neste caso, os dois bancos de dados proprietários levaram menos tempo em relação às opções gratuitas. No ambiente Linux, coincidentemente, quem levou a melhor foi o PostgreSQL, que é gratuito e de código-fonte aberto. Nos dois sistemas operacionais, o MySql foi a opção mais lenta, com resultados superiores a 20 minutos em ambos os testes.
Conclusão – Comparativo framework, banco de dados e sistema operacional
ambiente Windows estão disponíveis na tabela 3. Após os testes, podemos concluir que existiram A quantidade de registros manipulados foi a mes- combinações otimizadas, sendo que o Hibernate obma dos testes anteriores com a quantidade total de teve as cinco primeiras colocações em desempenho, 781.200 registros por operação. sendo seguido pelo TopLink em ambiente Linux. Dessa forma, conclui-se que a combinação de Tabela 3. Comparação de resultados entre bancos de tecnologias com configurações padronizadas, sem dados no Windows. qualquer customização, em um cenário que apresenBanco de dados Tempo total médio ta diversas entidades e diferentes tipos de relacionaSQL Server 0h19m37s mento para persistência com JPA é o Hibernate como Oracle 0h20m16s provedor de JPA em ambiente Linux, com um banco de dados PostgreSql, seguido muito próximo pelo PostgreSQL 0h21m52s banco de dados Oracle. Já a combinação menos veloz MySql 0h26m36s foi TopLink em ambiente Windows, utilizando a base Em ambiente Linux, obviamente, não foi consigratuita MySQL. derado o banco de dados da Microsoft. Os resultados mostraram-se próximos, expressando uma pequena Considerações finais vantagem do PostgreSql. Resultados como os descritos neste artigo mostram que, além das possibilidades de softwares proprietários Tabela 5. Tempo médio, considerando framework de persistência, OS e banco de dados. poderosos e confiáveis, as opções Tempo médio por Sistema Ranking Framework operacional Banco de dados operação gratuitas tam1 Hibernate Linux PostgreSQL 0m20s632ms bém devem ser consideradas. 2 Hibernate Linux Oracle 0m20s646ms Outra afirmação 3 Hibernate Windows SQL Server 0m22s119ms que pode ser fei4 Hibernate Windows Oracle 0m22s636ms ta é que soluções 5 Hibernate Windows PostgreSQL 0m23s390ms gratuitas em am6 TopLink Linux PostgreSQL 0m30s324ms bientes proprie7 TopLink Linux Oracle 0m30s926ms tários obtiveram 8 Hibernate Windows MySql 0m35s619ms os piores resulta9 Hibernate Linux MySql 0m37s840ms dos. No entanto, 10 TopLink Linux MySql 0m45s991ms o SQL Server, em 11 TopLink Windows SQL Server 0m51s501ms ambiente Windo12 TopLink Windows Oracle 0m53s411ms ws, mostrou-se competitivo às 13 TopLink Windows PostgreSQL 0m58s624ms soluções gratui14 TopLink Windows MySql 1m04s155ms tas. 57 \
spring_
Profiles no
Spring 3.1 Melhore o controle de suas aplicações usando os profiles do Spring 3.1 .
U
Conheça o uso dos profiles presentes na próxima versão do framework Spring (3.1), que permitirá que você defina em tempo de execução os recursos que serão utilizados pelo sistema. Aqui será mostrado um exemplo prático de uma das possíveis aplicações dessa nova característica que é muito bem-vinda.
ma aplicação desenvolvida utilizando Spring e alguns de seus módulos é uma ótima opção, mas dependendo do seu uso pode ser trabalhoso e cansativo de usar em diferentes ambientes. Normalmente quando construímos um sistema, temos um ambiente local de desenvolvimento e um ambiente de produção, mas em algumas empresas temos mais ambientes, como, por exemplo, o ambiente de testes, para a equipe de qualidade testar, ou o ambiente de homologação, para o usuário validar o que foi feito. Para a mesma aplicação funcionar em diversos ambientes, faz-se necessária uma configuração extra, normalmente manual, que obriga a gerar sempre um pacote para cada ambiente. Imagine que acidentalmente um pacote que testes foi para o ambiente de produção; “cabeças podem rolar” por um descuido desses. Neste artigo, depois de uma breve introdução ao Spring, será explicado com detalhes um exemplo prático de como colocar o mesmo sistema em diferentes ambientes e tirar proveito dessa novidade da versão 3.1. Faremos uma aplicação Web extremamente simples que terá acesso a diferentes bancos de dados conforme o ambiente que estiver rodando: desenvolvimento ou produção.
cionamento dos beans, pode pular para o próximo tópico. Os beans são o coração do Spring, tudo gira em torno deles para qualquer coisa que você queira fazer. O framework é dividido em diversos módulos que vão adicionando funcionalidades que você pode usar sempre que precisar. No nosso exemplo, usamos o módulo de injeção de dependências para atribuir os valores aos nossos objetos automaticamente, o módulo JDBC que auxilia no trabalho com o banco de dados e também usamos o módulo Model-view-Controller para gerenciar a navegação dos objetos do banco de dados até a página Web. Toda a configuração no Spring era centralizada em arquivos xML, mas depois da popularização das anotações, surgiu o Spring Annotations e ele foi incorporado como padrão na versão 3, o que fez com que parte da configuração necessária do xML ficasse diluída nas anotações das classes Java. A configuração (Listagem 1) é feita declarando um filtro no arquivo web.xml e mapeando para extensão que desejar. Opcionalmente, mapeia-se para o arquivo de configuração do Spring, onde serão lidas todas as configurações de todos os módulos que sua aplicação usará.
Conhecendo o Spring
Spring.
Se você já conhece o framework e entende o fun-
/ 58
Listagem 1. Arquivo web.xml com a configuração do
Fernando Boaglio | fernando@boaglio.com Bacharel em Ciências da Computação pela Unesp, foi instrutor oficial da Sun Microsystems e da Oracle Education. Atualmente contribui para alguns projetos open source, como KDE e Mentawai, e é da equipe de arquitetura da CodeIT Solutions, uma empresa especializada na prestação de serviços de desenvolvimento de software para as indústrias de seguros.
<context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/springapp-prod.xml</paramvalue> </context-param> <listener> <listener-class> org.springframework.web.context. ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>appServlet</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/springapp-prod.xml </param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>appServlet</servlet-name> <url-pattern>*.html</url-pattern> </servlet-mapping>
No exemplo deste artigo, temos um bean homeController que declara o mapeamento da classe HomeController feito pelo módulo MvC e logo abaixo o mapeamento dataSource que declara como será feito o acesso ao banco de dados, usado pelo módulo JDBC (Listagem 2).
tação Autowired criará uma nova instância de PessoaDAO sempre que a classe for chamada. Essa é uma das diversas facilidades que esse framework proporciona.
Listagem 3. Classe PessoaDAO responsável pela persistência de dados. @Repository(“pessoaDAO”) public class PessoaDAOImpl implements PessoaDAO { private HibernateTemplate hibernateTemplate; @Autowired public PessoaDAOImpl(SessionFactory sessionFactory) { this.hibernateTemplate = new HibernateTemplate(sessionFactory); }
}
Listagem 4. Classe controlador (do MVC) que manda os dados para a página Web. @Controller public class HomeController extends AbstractController { private PessoaDAO pessoaDAO;
Listagem 2. Beans do Spring.
@Autowired public HomeController(PessoaDAO pessoaDAO) { this.pessoaDAO = pessoaDAO; }
<bean id=”homeController” name=”/index.html” class=”br.com.mundoj.controller.HomeController”/>
@Override protected ModelAndView handleRequestInternal(HttpServletRequest req,HttpServletResponse res) throws Exception {
<bean id=”dataSource” class=”org.apache.commons.dbcp.BasicDataSource”> <property name=”driverClassName” value=”com.mysql.jdbc.Driver”/> <property name=”url” value=”jdbc:mysql://localhost/spring” /> <property name=”username” value=”root”/> <property name=”password” value=””/> </bean>
Com o uso de anotações, precisamos apenas configurar as classes como esse DAO (Listagem 3) para utilizá-lo no nosso controlador (Listagem 4), a ano-
public List<Pessoa> list() { List<Pessoa> l = this.hibernateTemplate.find( “from Pessoa”); return l; }
List<Pessoa> lista = pessoaDAO.list(); ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject(“pessoas”,lista); return modelAndView; } }
59 \