Começar a trabalhar em um grande projeto é sempre um desafio. Como entender o código se os desenvolvedores seniores - aqueles que mais poderiam te ajudar - provavelmente estarão muito ocupados? Ou quando a documentação é escassa e será necessário mostrar trabalho rapidamente. Como enfrentar essa situação? Este artigo oferece algumas sugestões.
1. Não tente entender toda aplicação de início
Não há motivos para buscar entender todo o código no começo de um projeto. Muito provavelmente, no início, será solicitada a reparação de um bug ou a melhoria de uma funcionalidade existente na aplicação. A primeira coisa que não se deve fazer é tentar entender toda a arquitetura da aplicação. Quando se está começando em um projeto, esta abordagem pode ser um pouco otimista demais.
Mesmo um desenvolvedor sênior com mais de 10 anos de experiência pode não entender certas partes da aplicação apesar de estar no mesmo projeto há mais de um ano (considerando que não seja da equipe inicial do projeto). O desenvolvedor sênior pode produzir bem e ainda assim não saber detalhes sobre o mecanismo de autenticação ou sobre os aspectos transacionais da aplicação, por exemplo.
Então como fazer? Um caminho é simplesmente procurar entender bem a parte em que se está trabalhando e procurar entregar valor para a equipe. Entregar valor hoje em dia é mais importante do que passar horas procurando entender algo que talvez te ajude somente no futuro.
2. Tenha foco em entregar valor imediatamente
Não seria importante, portanto, entender a arquitetura da aplicação? Sim. Entender a arquitetura é importante. Porém, entregar algo de valor o mais cedo possível deve ser o foco ao começar em um projeto. Uma vez preparado o ambiente de desenvolvimento, não se deve demorar mais do que uma ou duas semanas para entregar algo de valor, não importa o quão pequena seja tal entrega. Ser um programador experiente e não entregar algo em duas semanas, poderá gerar desconfiança nos gerentes e deixar uma má impressão.
O foco inicial, portanto, deve ser "entregar algo". Não é aconselhável explicar o fato de não entregar nada de valor com a justificativa de ser necessário estudar a arquitetura da aplicação. Isto não é justificável. Adicionar uma validação de Javascript pequena e específica pode gerar um valor considerável ao negócio e a expectativa inicial da gerência irá diminuir.
Com o passar das semanas e com a entrega de algumas correções e melhorias, toda a arquitetura da aplicação ficará aos poucos mais clara. É preciso ter cuidado para não subestimar o tempo necessário para entender todos os aspectos arquiteturais da aplicação. Talvez seja necessário alguns dias para entender o mecanismo de autenticação e um pouco menos para entender o gerenciamento de transações. O ponto-chave, entretanto, é entender algo por completo, não importa o tempo que isso leve.
É de grande importância a pré-existência de testes unitários bem escritos. Testes unitários são uma ótima maneira para entender grandes bases de código. Os testes unitários ajudam a começar pelas menores partes do código, entendendo as interfaces externas dos testes unitários (como uma unidade deve ser invocada e o que se deve esperar de retorno) e também suas implementações (analisar testes unitários é bem mais simples que analisar toda uma funcionalidade).
Ao compreender um conceito ou uma parte do sistema muito bem, é interessante também aproveitar para criar anotações ou diagramas de classes, de sequência e modelo de dados. Estes documentos também auxiliarão outros programadores no futuro.
3. Habilidades importantes para se manter grandes aplicações
Quando um programador é contratado para o trabalho, é necessário que apresente bons conhecimentos da tecnologia. Porém há outras habilidades que o ajudarão em grandes projetos de aplicações:
3.1 Capacidade de encontrar as principais classes rapidamente
Para qualquer tipo de atividade, seja correção de bug ou melhoria, a primeira tarefa é identificar quais as classes relacionadas à correção ou à melhoria. Uma vez identificadas as classes e/ou métodos necessários à correção, metade do trabalho estará feito.
3.2 Capacidade de analisar o impacto de uma mudança
Depois de fazer as mudanças necessárias para a correção de um bug ou uma melhoria de uma funcionalidade, o mais importante é garantir que tal mudança não afete outras partes do código. Com base nos conhecimentos da tecnologia em conjunto com os diversos frameworks existentes é possível deduzir onde as mudanças poderão impactar. Seguem dois exemplos sobre essa última afirmação:
a) Quando o método equals() de uma classe A é alterado, todas as chamadas para o método contains() em listas que contenham instâncias de A serão afetados. A menos que se saiba bem sobre a tecnologia empregada no caso, é bastante provável que não se perceba isto;
b) Em uma aplicação web, assumindo que o 'id do usuário' é armazenado na sessão. Um programador júnior pode associar algo ao 'id do usuário' como parte de uma correção de bug sem saber que esta ação irá impactar outros casos de uso que dependem do ´id do usuáio'.
Logo, é importante conhecer suficientemente bem tanto a linguagem da tecnologia como também os frameworks utilizados para se poder analisar os impactos de uma mudança.
Uma vez dominadas estas duas habilidades, a maioria das atividades de manutenção serão fáceis, mesmo sem ter muito conhecimento sobre a aplicação. Caso se trate da correção de um bug, é necessário primeiro localizá-lo no código, corrigi-lo e garantir que a correção não afete nenhuma outra parte da aplicação. Caso seja o desenvolvimento de uma melhoria ou implementação de uma nova funcionalidade, na maioria das vezes, basta seguir a estrutura de uma funcionalidade existente.
Isso posto, considerando uma aplicação bancária, toda a arquitetura de uma funcionalidade, como por exemplo "Resumo da Conta", não irá diferir muito de outras funcionalidades, como por exemplo "Histórico de Transações". Uma vez entendida a arquitetura de "Resumo da Conta" é possível utilizá-la como base para criação da arquitetura de "Histórico de Transações".
Em resumo, não é necessário entender qual o papel de todas as 2000 classes de um sistema ou todo o fluxo do código de uma aplicação para realizar correções ou melhorias. Com as habilidades descritas anteriormente é possível identificar as partes do código que necessitam de alterações, realizar as alterações com base nos conhecimentos da tecnologia e de seus frameworks, garantir que as alterações não afetem outras partes do sistema e, por fim, entregar correções e melhorias mesmo com pouco conhecimento da arquitetura da aplicação.
4. Ferramentas para encontrar oportunidades de melhoria e para conhecer o impacto de uma mudança
Continuando com a temática da importância de se entregar algo de valor o quanto antes, deve-se procurar ferramentas que contribuam para a entrega de valor, mesmo com pouco conhecimento sobre a aplicação.
4.1 Ferramentas para encontrar oportunidades de melhoria
Tanto para correções quanto para melhorias, a primeira coisa a se fazer é encontrar qual a classe ou o método correspondente. Há basicamente duas abordagens para se entender uma funcionalidade - análise do código-fonte e análise da funcionalidade em tempo de execução.
Ferramentas de análise de código-fonte realizam uma varredura por todo o código e mostram as relações existentes entre as classes. Há várias técnicas de análise de código-fonte e há várias ferramentas no mercado. Alguns exemplos são: Architexa, AgileJ, UModel, Poseidon, etc.
Todas essas ferramentas fazem a análise do código-fonte, porém não conseguem dizer com precisão todas relações entre classes e chamadas de métodos que ocorrem em tempo de execução. A razão disto é a utilização de load [carregamento] em tempo de execução, utilização de métodos de callbacks [rotina para tratamento de evento], etc. Por exemplo, nenhuma ferramenta consegue inferir qual servlet [componente do lado servidor que gera dados HTML e XML] será chamado quando um determinado botão for pressionado.
Há ferramentas que realizam a análise do código em tempo de execução determinando quais classes e métodos são chamados. Algumas dessas ferramentas são: MaintainJ, Diver, jSonde, Java Call Tracer, etc. Essas ferramentas geralmente capturam a sequência de chamadas em tempo de execução e se utilizam destas informação para gerar diagramas de sequências e de classes para cada caso de uso.
O diagrama de sequências mostra todas as chamadas a métodos em tempo de execução. Logo, tratando-se da correção de um defeito, o problema provavelmente estará em algum dos métodos chamados.
Tratando-se de melhorias, é importante entender o diagrama de sequências de uma determinada funcionalidade e, a partir daí, melhorá-la. Tal melhoria poderá ser a adição de uma validação, ou a alteração de um DAO [Data Access Object], etc.
Ao desenvolver uma nova funcionalidade, deve-se procurar uma funcionalidade semelhante, entender o diagrama de sequência de tal funcionalidade e utilizá-lo como base para o novo desenvolvimento.
Escolha a ferramenta de análise com cuidado. Um grande problema relacionado com essas ferramentas consiste no alto grau de detalhes que algumas delas geram. Deve-se escolher uma ferramenta que permita filtrar detalhes desnecessários, de maneira a facilitar o entendimento.
4.2 Ferramentas para conhecer o impacto que as mudanças podem causar
Se casos de teste estão disponíveis, é necessário executá-los para garantir que as mudanças não afetarão o resto dos testes. Provavelmente, os testes unitários não irão cobrir todo o código de uma grande aplicação empresarial. Nessas situações, é interessante procurar a ajuda de alguma ferramenta.
De novo, as duas maneiras de realizar a análise são em tempo de compilação e em tempo de execução. Há várias ferramentas de análise de código em tempo de compilação presentes no mercado. Alguns exemplos são: Lattix, Structure101, Coverity, nWire and IntelliJ's DSM.
Dada uma determinada mudança em uma classe, as ferramentas citadas no parágrafo anterior realizam uma análise no código em tempo de compilação em busca das dependências entre as classes. Com estas informações, os programadores deduzem quais as classes afetadas pela mudança dado que a ferramenta não consegue determinar quais classes invocarão o método em tempo de execução.
Não há muitas ferramentas que realizam uma análise do código em tempo de execução. Uma das poucas ferramentas é a MaintainJ, que começa identificando todas as classes e métodos envolvidos em uma determinada funcionalidade. Uma vez em posse dessa informação, é possível identificar as funcionalidades impactadas pelas mudanças em um determinado conjunto de classes. Uma pré-condição para o funcionamento do MaintainJ é a de que todas as funcionalidades devem rodar ao menos uma vez para que as dependências possam ser capturadas.
Em resumo, atualmente há pouca ajuda de ferramentas para a análise de impacto de uma mudança. Primeiro é necessário identificar a forma correta de realizar a análise de impacto e determinar qual será o impacto da mudança, que dependerá do resultado dessa análise ou a de um desenvolvedor sênior. As ferramentas citadas acima devem ser utilizadas como suporte às decisões tomadas.
5. Duas ressalvas com relação aos argumentos acima
5.1 Não comprometa a qualidade do código
O fato de se buscar entregas rápidas não deve ser usado como desculpa para comprometer a qualidade do código. Os exemplos a seguir demonstram situações em que pode ser tentador abrir mão da qualidade do código em troca de uma entrega rápida.
A adição de novas funcionalidades geralmente possui um risco menor do que a alteração de uma funcionalidade já existente com inúmeras dependências. Por exemplo, pode existir um método que faça parte de cinco funcionalidades diferentes. Como parte de uma melhoria de uma dessas funcionalidades, será necessário realizar alterações na implementação desse método. O mais fácil a se fazer pode ser copiar o método, renomeá-lo e chamá-lo na mudança que se está desenvolvendo. Porém isso nunca deve ser feito. Duplicação de código é ruim e para evitar que isso aconteça deve-se verificar a possibilidade de construir um wrapper para este método, sobrescrevê-lo ou simplesmente alterá-lo e testar novamente todos os casos de uso.
Outro exemplo é a mudança de um método de 'privado' para 'público' para que possa ser invocado por uma outra classe. É sempre ruim expor mais do que o necessário. Se um pouco de refactoring for necessário para não ter que expor um método privado como público, deve-se realizar o refactoring.
A maioria das aplicações possuem uma certa estrutura e um padrão. Mesmo na realização de uma correção ou na adição de uma melhoria, deve-se manter tais padrões. Na dúvida, um desenvolvedor sênior deve avaliar as alterações implementadas. Caso não seja possível seguir as convenções, deve-se ter cuidado para que isso fique de forma localizada, em pequenas classes (um método privado em uma classe de 200 linhas por exemplo).
5.2 Entender a arquitetura no médio / longo prazo
Seguindo o que foi sugerido neste guia, entregar algo de valor o mais breve possível entendendo o mínimo necessário sobre a arquitetura é mais importante que passar grande tempo entendendo toda a arquitetura da aplicação. Porém, deve-se tomar cuidado para que o contrário também não ocorra, ou seja, sempre trabalhar em entregas rápidas e pontuais e não se preocupar em conhecer toda a arquitetura da aplicação.
6. Conclusão
O foco do artigo é em como entregar algo rápido, aprendendo o necessário, sem comprometer a qualidade do código.
No caso de uma correção, encontrar o bug e corrigí-lo rapidamente é muito importante. As ferramentas de análise de código em tempo de execução podem ajudar. No caso de uma melhoria, uma funcionalidade similar pode ser utilizada como exemplo para entender o fluxo e implementar a melhoria.
Tudo isso pode parecer bem simples, mas é possível aplicá-lo na prática? Sim. Mas a pré-condição é de que os conhecimentos básicos necessários sobre a tecnologia e frameworks são fundamentais para realizar as primeiras alterações no código e então analisar o impacto da mudança. O que mais se destaca é a capacidade de analisar o impacto de uma alteração, mais do que fazer a alteração em si. A ajuda de um desenvolvedor sênior para analisar o impacto é sempre de grande importância.
Aproximadamente 50% de todo o custo operacional em TI é gasto com correções e pequenas melhorias. Seguindo o proposto neste artigo é possível economizar uma quantidade considerável de dinheiro.