Autor: Luiz Ricardo (Página 5 de 16)

Quando usar o Domain-Driven Design (DDD)

DomainDrivenDesign

Domain-Driven Design ou Projeto Orientado ao Domínio é um padrão de modelagem de software orientado a objetos que procura reforçar conceitos e boas práticas relacionadas à Orientação a Objetos.

Isso vem em contrapartida ao uso comum do Data-Driven Design ou Projeto Orientado a Dados, que a maioria dos desenvolvedores usa sem mesmo ter consciência disso.

Data-Driven Design

Já ouvi várias vezes que os dados são a coisa mais importante em uma empresa, logo a modelagem deve sempre começar pensando-se no banco de dados. Esta é a abordagem algumas vezes chamada de database first, ou “banco de dados em primeiro lugar”.

Não é nada incomum desenvolvedores .Net, Java e C++ começarem um sistema estabelecendo os tipos que eles vão usar e o relacionamento entre eles, como se estivessem criando um Modelo Entidade-Relacionamento (MER). Esses tipos geralmente são objetos “burros”, com getters e setters, representando nada mais, nada menos, que uma tabela do banco de dados.

O problema com essa abordagem é que ela não faz bom uso dos recursos da Orientação a Objetos. Muitos acham que getters e setters são o auge do encapsulamento, mas na prática esses métodos permitem ao usuário recuperar e alterar todos os atributos. Não há ganho algum, a não ser muito código desnecessário.

Enfim, muita gente acha que está usando OO, mas as classes poderiam ser facilmente substituídas por registros ou estruturas de dados mais simples, de acordo com a linguagem utilizada.

Se você consegue ler em Inglês, uma discussão interessante sobre isso está no artigo Dance you Imps!, escrito por Rebert Martin e também conhecido como Uncle Bob.

Domain-Driven Design (DDD)

A ideia inicial do DDD é voltar à uma modelagem OO mais pura, por assim dizer.

Devemos esquecer de como os dados são persistidos e nos preocupar em como representar melhor as necessidades de negócio em classes e comportamentos (métodos). Esta é uma abordam também conhecida como como code first, ou “código em primeiro lugar”.

Isso significa que em DDD um Cliente pode não ter um setter para os seus atributos comuns, mas pode ter métodos com lógica de negócio que neste domínio de negócio pertencem ao cliente, como void associarNovoCartao(Cartao) ou Conta recuperarInformacoesConta().

Em resumo, as classes modeladas e os seus métodos deveriam representar o negócio da empresa, usando inclusive a mesma nomenclatura. A persistência dos dados é colocada em segundo plano, sendo apenas uma camada complementar.

Quando não usar DDD

Às vezes só é necessário um CRUD

DDD não é uma solução para tudo. A maioria dos sistemas possui uma boa parte composta por cadastros básicos (CRUD) e pelo menos nessa parte não há necessidade de um modelo elaborado de dados.

O DDD deve ajudar na modelagem das classes mais importantes e mais centrais do sistema de forma e diminuir a complexidade e ajudar na manutenção das mesmas, afinal este é o objetivo dos princípios de orientação a objetos.

Compartilhando dados com outros sistemas

Rotinas de integração que recebem ou disponibilizam dados para outros sistemas não devem ser “inteligentes”.

Muitos desenvolvedores acabam modelando suas classes de negócios tentando resolver as questões internas do sistema e, ao mesmo tempo, pensando em como essas classes serão expostas para outros sistemas.

Padrões como DTO (Data Transfer Object) que usam objetos “burros” são mais adequados para isso.

Considerações finais

O DDD não tenta resolver todos os problemas de todas as camadas de um sistema.

Seu foco é na modelagem das entidades principais de negócio usando a linguagem adequada daquele domínio para facilitar a manutenção, extensão e entendimento.

Particularmente, eu não seguiria à risca o padrão, até porque existem inúmeros padrões e variações de modelagem OO.

Porém, é sempre importante estudar e entender os princípios por detrás desses padrões para, dessa forma, aplicar o que funciona melhor para cada situação.

Referências

  1. DDD – Introdução a Domain Driven Design
  2. Coding for Domain-Driven Design: Tips for Data-Focused Devs
  3. Domain Driven Design Quickly (e-book gratuito)

Nota: escrevi termos como domain-driven com hífen, pois quando duas ou mais palavras formam um adjetivo composto no Inglês elas geralmente devem ser “ligadas”. No caso, domain-driven é um adjetivo de design. (Referência)

As software engineers, what are we really building?

bridge

I never liked the bridge building metaphor very much and I know many software developers also didn’t. Many criticize the whole engineer thing and prefer the craftsmanship metaphor.

I can see value in both, but I wasn’t able to express what exactly was the problem with them. At least until a few days ago, when I watched this cartoon with my son about a tractor loader stacking numbered little cubes of various colors.

The building block metaphor

blocks

The first thing that came to my mind was the analogy of building software using components. If we think on these components as blocks, we can go even further. When we stack a block on the top of another or gather them side by side we can call the surface touching another block’s surface as the interface between them.

The problem is, in software, greater contact area between two components means coupled code that’s hard to change and maintain, right? However, real blocks benefit from friction and are more stable when they have direct contact with a greater number of blocks. While real blocks support each other, too much friction between software components makes them unusable.

So, when you think a little deeper in this analogy, you realize it didn’t work very well.

The bridge metaphor

Going back to the bridge analogy, after having watched that cartoon, I realized a subtle but crucial difference between a real bridge and that one that fills the gap between client needs and running software.

When professionals build a house, a building, or a bridge they go to the physical place, inspect, measure, do the calculations, bring the material, put each thing in their place. If some material is missing, they just buy more. If something is not fitting in, they just tweak it. When they finish it, they can see exactly whether they met the expectations or not. Of course a bridge can fall, someone can do incorrect calculations, but in general engineers are somehow able to assess its stability.

What can I say about software development? It’s abstract. It can’t be seen. It can’t be measured properly. You can test it in some manners, but it doesn’t guarantee it’s correct. But you already knew about all these characteristics, right? Yet there’s something perhaps you have missed, as I did. That is, in software, we don’t just build and delivery.

Actually, we do not build a bridge at all. What we really build is an abstraction. Not an abstract bridge. We build an abstract bridge builder.

At low level, every time an application is executed it builds a concrete bridge. That bridge needs to fit perfectly unknown places and shouldn’t fall under uncertain weights. In another words, it brings and loads and gathers and connects all components, and when they’re ready it’ll do whatever it was designed to do.

So a software bridge is more like a bridge you can carry in your pocket and use whenever you need to cross some valley or river. Depending on the user, he/she will complain that bridge won’t take them from America to Europe.

And about the Mona Lisa?

I think we can apply the same principle to software craftsmanship. Think for a moment. Even if we switch to this analogy, with developers producing binary masterpieces and programmers painting Mona Lisa’s, it doesn’t change anything in the nature of that problem.

We’re still making an abstract artwork builder. When the software runs, it should creates instantaneously a masterpiece according to the instructions of its master.

Is there a proper analogy?

Sorry for disappointing you, but in fact I have no idea. 🙁

On the other hand, I have strong reasons to think that much of our problems in software engineering will be solved by finding the right analogy.

As cubes and rectangles and circles are properly used as abstractions to build bridges and paint masterpieces, it there should be proper abstractions to software development.

One thing I do know

As software developers (engineers or craftsmans) we should be simple. We need master simplicity.

I think it’s clear for anyone who have ever worked with software that complexity should be avoided at all costs. It’s not by chance the latest frameworks and tools and processes and methodologies and techniques try to be as simple as possible.

Nevertheless, I have something against them. They’re becoming too shallow.

For instance, I’ve seen many people doing agile because they hate documentation. But throwing it away won’t solve their problem. I’ve seen many new flavors of tools and frameworks and methodologies that do exactly what the existing ones do, except for promising they’re easier. I’ve seen people that hate everything that seems to be old and love every novelty though they have no clue about the reason.

They just want to work less. They just want to think less.

We’re living the era of lazy programmers. But not the kind that would automate repetitive tasks, as noticed by Steve McConnell in his book Code Complete, or avoid useless documentation. Unfortunately, there are plenty of lazy programmers today that don’t even want to understand the problems they face. They want some magic easy solution so they don’t need to think and do the hard work.

Simple but deep

When you have a problem, there’s basically two alternatives: ignore it and hope that soon or later it’ll disappear, or face it and do whatever it takes to solve it.

Despite many advances in the field, I think we’re still living a software crisis. I see many companies and professionals completely lost having so many options to do the same thing, while others jump from technology to technology by a matter of taste.

Well, I have my personal taste for programming languages too, but that’s not the point. I’m talking about the fundamentals. I think people are just attacking the wrong thing. They’re ignoring the real problem.

It’s not sufficient to make things easy and cool without solving the real problem.

Software development today is simpler than some decades ago. Nonetheless, as spotted in Brooks’ Mythical Man-Month, it happened because we overcome the accidental difficulties of programming. We have now better debugging, rich-featured IDEs, faster personal computers, and son on. But few initiatives attack the essence of the complexity in software development. And here dwells our biggest challenge.

In order to make software development even simpler, it’s necessary to dig deep into the dirty of complexity, face the hard things, try solution after solution, year after year.

We need people that do not put blind faith in new tools and methods, that aren’t lazy to do whatever needs to be done, that really understand what they are doing. And we need people willing to persist in this direction, instead of being apathetic and choosing the easy path.

Only those who discipline themselves in this craft will be able to provide us with a real improvement in software development some day.

Entendendo as Configurações de Memória da Máquina Virtual Java (JVM)

java

Talvez você já tenha ouvido falar que em Java não é preciso preocupar-se com gerenciamento de memória, ou ainda que em Java a memória nunca acaba por causa do Garbage Collector (GC).

Bem, estou aqui para lhe dizer que tudo isso é besteira. Depois de erros banais como NullPointerException e SqlException, os problemas mais sérios e comuns que tenho visto ao longo dos anos são justamente relacionados à memória, isto é: OutOfMemoryError.

Seja por falta de configuração adequada ou implementação equivocada. Código ruim pode alocar objetos indeterminadamente sem remover as referências a eles, de forma que nem o GC dá um jeito. Porém, neste artigo vamos focar na parte de configuração.

O erro OutOfMemoryError manifesta-se em duas formas principais: Java Heap Space e PermGen Space. Ambos são, respectivamente, relacionados a trechos de memória dinâmico e permanente do Java.

A maioria das aplicações sérias passa, em algum momento, por ajustes na quantidade de memória disponível tanto para a seção dinâmica quanto para a seção permanente.

Vamos ver o que é tudo isso e como efetuar a configuração.

Heap Space (Memória dinâmica)

O Heap Space é o local onde o Java armazena suas variáveis e instâncias de objetos. Este espaço de memória pode aumentar ou diminuir ao longo do tempo, dinamicamente, de acordo com a quantidade de objetos usados no programa.

Para definir quanta memória pode ser usada pelo Java, é preciso informar parâmetros ao executar a JVM. Caso você não saiba, executar o java cria uma nova instância da JVM, portanto qualquer parâmetro passado a este programa é uma configuração para essa JVM.

Já dei uma breve introdução sobre isso no artigo Instalando, Configurando e Usando o Eclipse Kepler, porém cada programa pode ter seu modo de configurar. Se você está usando um programa qualquer ou um servidor de aplicação (JBoss, Websphere, Weblogic, Glassfish, Tomcat, etc.), procure na documentação do mesmo onde fica a configuração dos parâmetros de memória.

Neste artigo, vou exemplificar uma chamada diretamente ao comando java via linha de comando.

Quantidade máxima de memória dinâmica

O parâmetro Xmx define a quantidade máxima de memória dinâmica que a Máquina Virtual Java pode alocar para armazenar esses objetos e variáveis.

É importante definir uma quantidade máxima de memória razoavelmente maior do que a média usada na aplicação para evitar não só OutOfMemoryError como também escassez de memória.

Trabalhar no limite da memória disponível faz o Garbage Collector executar muitas vezes para coletar objetos não usados e isso resulta em pausas indesejadas no programa durante a varredura dos objetos.

Quantidade inicial de memória dinâmica

O Xms define a quantidade inicial de memória dinâmica alocada no início da JVM.

É importante verificar quanto sua aplicação usa em média e definir um valor próximo disso. Dessa forma, não ocorrerão muitas pausas para alocação de memória, resultando em um desempenho maior de inicialização até o ponto em que a aplicação está executando num patamar estável.

PermGen Space (Memória permanente)

O Java também possui a PermGen Space, outra parte da memória chamada de “estática” ou “permanente”, utilizada para carregar suas classes (arquivos .class), internalizar Strings (string pool), entre outras coisas.

Como regra geral, a memória permanente não pode ser desalocada. Isso implica que, por exemplo, se sua aplicação tem muitos Jars e carrega muita classes, em algum momento poderá ocorrer um erro de PermGen Space. Isso é comum com quem abusa de frameworks com modelos de classes “pesados”, sendo um cenário comum a turminha JSF, PrimeFaces, Hibernate e JasperReports.

O erro ocorre porque não é possível ao Java carregar novas classes quando não há espaço e não é mais possível aumentar a memória permanente, pois não dá para descartar classes já carregadas para dar lugar a novas.

Da mesma forma que na memória Heap, é possível informar à JVM a quantidade de memória permanente máxima que pode ser alocada e a quantidade de memória permanente inicialmente alocada.

O parâmetro XX:MaxPermSize define a quantidade máxima de memória permanente que a JVM pode utilizar e o parâmetro XX:PermSize define o tamanho inicial alocado.

Os mesmos princípios mencionados anteriormente para definição de quanta memória Heap deve ser reservada podem ser aplicados aqui.

Gerações de Objetos

A seção dinâmica da memória, Heap Space, é ainda dividida entre “nova” e “velha” geração.

A seção da “nova geração” (Young Generation) é reservada para a criação de objetos. É o berçário do Java. Estatisticamente, os objetos recém-criados são mais suscetíveis a serem coletados.

Após ter algum tempo de vida, o GC pode promover os objetos que não forem coletados para a seção da “velha geração” (Old Generation), onde ficam os objetos com mais tempo de vida e com menos chances de serem coletados.

Os parâmetros XX:MaxNewSize e XX:NewSize definem, respectivamente, a quantidade máxima de memória reservada para os objetos da “nova geração” e a quantidade inicial de memória para tais objetos.

Note que a memória da “nova geração” fica dentro do espaço do Heap Space, portanto cuidado com os valores definidos.

Nunca tive a necessidade de modificar esses parâmetros e creio que na grande maioria dos casos você também não vai precisar. Use-os somente em caso de última necessidade.

Ilustração da divisão de memória da JVM

A imagem a seguir ilustra os conceitos apresentados no artigo:

java-memory

A memória total alocada pelo Java é a soma do Heap Space e PermGen Space. O espaço para a Young Generation fica dentro do Heap.

Exemplo

Cada programa Java tem sua forma de configuração, porém ao executar diretamente o comando java, você pode simplesmente passar os parâmetros da seguinte forma:

java -Xmx2g -Xms1024m -XX:MaxPermSize=1g -XX:PermSize=512m

No exemplo acima, definimos:

  1. -Xmx2g: Quantidade máxima de memória dinâmica de 2 Gigabytes
  2. -Xms1024m: Quantidade inicial de memória dinâmica de 1024 Megabytes ou 1 Gigabyte
  3. -XX:MaxPermSize=1g: Quantidade máxima de memória permanente de 1 Gigabyte
  4. -XX:PermSize=512m: Quantidade inicial de memória permanente de 512 Megabytes

Considerações

As configuração de memória apresentadas neste artigo resolvem 90% ou mais dos problemas cotidianos relacionados à memória com a JVM. Entretanto, este é apenas o tipo de ajuste mais básico que existe.

Você pode encontrar informações bem mais detalhadas, incluindo parâmetros de configurações específicas, em artigos voltados para otimização da JVM. Por exemplo:

Por fim, é importante lembrar que não existe uma fórmula mágica para determinar valores de memória para um sistema. Antes, deve ser levado em conta o tipo de uso, recursos utilizados, quantidade de usuários e capacidade do hardware.

Não creio que isso possa ser determinado objetivamente por alguma fórmula, mas através de um processo dedutivo conduzido por um profissional experiente.


Este artigo foi baseado em minha resposta no Stack Overflow em Português.

Depuração Avançada de JavaScript

js-logo-badge-512

Ainda me lembro do tempo em usava alerts para encontrar erros em JavaScript. :/

Hoje todo navegador tem sua ferramenta de ajuda ao desenvolvedor. Basta pressionar F12 (pelo menos no Chrome, Firefox e Internet Explorer para Windows) em qualquer página e você já pode “hackear” o HTML, CSS e JavaScript, monitorar o tempo de carregamento total ou de cada artefato pertencente ao site, depurar JavaScript linha a linha e muito mais.

Infelizmente, muitos desenvolvedores ainda estão limitados às técnicas arcaicas de depuração JavaScript que consistem praticamente em tentativa e erro, por exemplo inspecionando valores com a função alert. Alguns avançaram um pouco e usam o console.log.

Veremos a seguir algumas técnicas um tanto menos triviais para obter uma experiências mais agrádavel ao desenvolver e testar páginas web.

Depurando Javascript

No Chrome, navegador desenvolvido pelo Google e meu predileto, você pode facilmente depurar a execução de código JavaScript.

Para isso, basta acessar a aba Source da Ferramenta do Desenvolvedor. Ali é possível abrir qualquer asset como CSS e JavaScript, editá-los, salvá-los e ver o resultado da edição sem recarregar a página.

Nos fontes JavaScript você pode definir breakpoints como em qualquer IDE de desenvolvimento. Quando aquele trecho for executado, a execução será interrompida. Então você pode inspecionar variáveis e avançar a execução linha por linha.

01-breakpoint

Porém, embora a depuração com breakpoints seja uma tremenda mão-na-roda, ela nem sempre é suficiente.

Depuração Avançada

O Chrome disponibiliza através do console uma API de linha de comando que possibilita meios mais avançados para depuração. Vejamos algumas funcionalidades interessantes nos tópicos a seguir.

Monitorando eventos

Breakpoints funcionam bem, pelo menos até precisarmos depurar um caso mais complexo envolvendo eventos de teclado e mouse.

Imagine o cenário onde você está trabalhando em uma tela que outros desenvolvedores enfeitaram com vários scripts que usam eventos como mouse move, mouse up, mouse down, key up, key down, etc. Agora sobrou para você corrigir um bug em algum dos vários eventos.

Breakpoints podem não ser adequados para analisar o comportamento dos scriots, pois interromper a sua execução no meio de um evento acaba alterando o resultado final da execução, afinal outros eventos deixarão de ocorrer ou não acontecerão na sua sequência natural. Colocar logs em todos os eventos não é muito produtivo nem viável em alguns casos.

Para facilitar esta tarefa, a API do Chrome tem a função monitorEvents, que permite ao desenvolvedor listar os eventos que ocorrem em um objeto.

Um exemplo para capturar eventos do mouse na página é:

monitorEvents(document.body, 'mouse');

Após a execução deste comando, qualquer ação do mouse na página vai gerar um log no console.

02-console-monitor-events

Para desativar o monitoramento, execute o método unmonitorEvents com os mesmos argumentos de antes. Exemplo:

unmonitorEvents(document.body, 'mouse');

Listando manipuladores de eventos (listeners) associados a um objeto

Depurar uma página complexa é uma tarefa árdua. Em algumas situações, é quase impossível saber quais eventos estão associados a quais objetos. Uma função interessante da API de comandos do Chrome permite descobrir exatamente isso: getEventListeners.

O exemplo a seguir mostra os listeners associados ao documento:

getEventListeners(document);

03-listeners

Monitorando chamadas a funções

Outro caso onde breakpoints não resolvem é quando funções são executadas várias vezes em sequência. Neste caso, podemos monitorar cada chamada realizada a determinadas funções, inclusive quais argumentos foram passados, através da função monitor da API do Chrome. Basta passar o nome de uma função por parâmetro:

monitor(minhaFuncao);

Isso vai gerar um log para cada execução.

04-monitor

Para deixar de monitorar as chamadas, use a função unmonitor:

unmonitor(minhaFuncao);

Analisando o desempenho da aplicação

Você pode testar o desempenho da execução dos scripts através da aba Profile da ferramenta do desenvolvedor.

Entretanto, para testes mais específicos é interessante poder iniciar e parar a análise de consumo de CPU programaticamente. Isso pode ser feito através das funções profile e profileEnd.

Dessa forma, você pode analisar as execuções inclusive de forma intercalada:

profile("A");
profile("B");
profileEnd("B");
profileEnd("A");

05-profiles

Outros navegadores

Vale lembrar também que várias dessas funcionalidades de depuração não estão limitadas ao Google Chrome. Praticamente todos os navegadores modernos possuem uma API e um console onde você pode manipular a página.

O plugin Firebug tem uma API bem completa para depuração. Mesmo sem plugin, o Firefox disponibiliza uma API nativa. O mesmo vale para o Safari da Apple. Até o Internet Explorer tem vários métodos em sua API.

Já a API do Opera não parece contar com muitos comandos avançados, mas possui diversas utilidades.

Conclusões

Quem trabalha com front-end ou qualquer outra camada de um sistema web pode acabar, cedo ou tarde, tendo que consertar algum JavaScript.

Portanto, é sempre bom acompanhar a evolução das ferramentas de depuração de páginas web. Aprender as funcionalidades um pouco mais avançados certamente irá lhe economizar tempo e desgaste desnecessários.

Materiais para uso em aulas, cursos e palestras

Estou disponibilizando neste post uma compilação dos materiais que já usei em aulas, cursos e palestars.

Licença

Você pode usá-los livremente.

A licença dos documentos que tenho produzido é Creative Commons, a mesma deste blog.

Você é livre para usar e modificar o material livremente. Só peço que cite a fonte original (meu blog, por exemplo).

Google Drive

Veja a lista de materiais disponíveis no Google Drive:

Todas as pastas acima estão contidas numa única pasta do Google Drive.

SlideShare

Os slides também estão disponíveis no SlideShare:

Considerações

Os materiais não são feitos para estudo individual. Ele não é exaustivo e depende do professor ou palestrante ter conhecimento sobre o assunto. No entanto, deixei comentários e informações adicionais sobre cada um, basta usar a função de notas do SlideShare ou baixar o PPT.

Se quiser usar o material em suas aulas ou cursos e tiver dúvidas sobre algum ponto, escreva para mim.

Também fique à vontade para enviar correções ou melhorias que fizer. 😀

Pensando TDD

Ontem, dia 8 de outubro de 2014, ministrei uma palestra sobre TDD na Faculdade de Tecnologia de Sorocaba, durante a 21ª edição da Semana de Tecnologia, evento anual promovido pela faculdade para aproximar os alunos das práticas mais modernas do mercado.

TDD or not TDD?

Test-Driven Development (Desenvolvimento Orientado a Testes), ou simplesmente TDD, é uma disciplina de desenvolvimento ágil que, resumidamente, prioriza testes automatizados ainda na fase de projeto com o objetivo de obter software de qualidade, isto é, com código limpo e que funcione.

Indo direto ao ponto, TDD não é sobre TDD, mas sobre a disciplina e prática de como torna-se um bom Engenheiro ou Artesão de Software, dependendo da metáfora que você adota.

Embora esse assunto não seja novidade, é de muita importância para a maior parte dos profissionais de TI brasileiros, sem contar as empresas, pois infelizmente estamos de forma geral muito defasados com as melhores práticas reconhecidas no restante do mundo.

Veja, a redescoberta do TDD já tem mais de uma década e a maioria dos profissionais que encontro nunca viu um projeto que realmente aplicasse a metodologia. E o problema não é somente com relação ao TDD. Parece que somente agora empresas estão começando a aceitar certas práticas geralmente adotadas por processos ágeis, tais como testes automatizados, integração contínua, reuniões diárias, programação em par, etc.

A palestra, intitulada “Pensando TDD”, também não é necessariamente sobre TDD, mas sobre a necessidade de praticarmos o desenvolvimento de software como uma disciplina diária e não com a “lei do mínimo esforço” que, com o tempo, gera um déficit técnico tão grande que não resta alternativa a não ser literalmente jogar o software no lixo.

Material

Os slides da apresentação estão disponíveis no SlideShare e no Google Drive. Muitos deles são conceituais. Para ver comentários e informações adicionais sobre cada um, use a função de notas do SlideShare ou baixe o PPT.

Lembrando que a licença dos documentos que tenho produzido é Creative Commons. Você é livre para usar e modificar o material livremente. Só peço que cite a fonte original (meu blog, por exemplo).

Se tiver dúvidas sobre algum ponto, escreve para mim.

Agradecimentos

Por fim, gostaria de agradecer à organização do evento e à diretoria da Fatec de Sorocaba, assim como à GFT, empresa onde trabalho, por incentivar a participação em eventos e comunidades de tecnologia.

Obrigado também a todos os alunos que se mostraram muito receptivos a novos conhecimentos e suportaram a hora e meia comigo falando. Sucesso para todos vocês!

Trabalhando com Genéricos em Java

cup_generics Trabalhar com genéricos é uma abordagem muito produtiva para reutilização de código.

Exemplo: DAOs genéricos

Implementações genéricas de DAOs (Data Access Object) é algo que vejo repetidas vezes em diferentes projetos e equipes. Implementando alguns métodos genéricos, é possível evitar a criação de dezenas de classes semelhantes num projeto. Se bem planejado e usado, técnicas como essa economizam tempo e facilitam a manutenção.

Em Java, podemos criar uma classe base com um tipo genérico para representar nosso DAO. Considere o seguinte trecho de código:

public class BaseDao<T, ID> {
    public T find(ID entityId) { /*...*/ return null; }
    public void insert(T entity) { /*...*/ }
    public int update(T entity) { /*...*/ return 0; }
    public void delete(ID entityId) { /*...*/ }
}

Há duas principais abordagens para usar uma classe genérica como essa.

Herança

Podemos estender a classe BaseDao, gerando subclasses específicas que estendem as funcionalidades padrão.

Exemplo:

public class ClienteDao<Cliente, Integer> { ... }

public class LoginDao<Login, String> { ... }

Uso direto

Quando não há necessidade de especialização, é possível usar diretamente a classe base.

Exemplo:

BaseDao<Cliente, Integer> baseDao = new BaseDao<>();

BaseDao<Login, Integer> loginDao = new BaseDao<>();

Identificando o tipo genérico

Em determinadas situações é desejável descobrir em tempo de execução qual é o tipo genérico de uma classe.

Continuando em nosso exemplo do DAO, poderíamos ler alguma anotação ou configuração associada à classe Cliente quando o DAO fosse deste tipo.

Como fazer isso nos dois tipos de DAOs mencionadosacima?

Herança

Considere o código:

public class BaseDao<T, ID> {

    final private Class<T> entityType;

    public BaseDao() {
        entityType = (Class<T>) ((ParameterizedType)
                getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    //...

}

Supondo que existem subclasses, tal como ClienteDao extends BaseDao<Cliente, Integer>, então o atributo entityType receberá a referência para a classe Cliente.

Trechos de código semelhantes a esse estão disponíveis em várias postagens e respostas do Stack Overflow, mas é algo que pode ser considerado “inseguro”, pois há cenários em que isso simplesmente não funciona.

Uso direto

O código do construtor, como exibido acima, não funciona numa instanciação direta de BaseDao.

O seguinte exemplo vai gerar um erro:

 BaseDao<Login, Integer> loginDao = new BaseDao<>(); //não vai instanciar o objeto

Qual o motivo?

Type erasure

Primeiro, é preciso entender que a informação do tipo genérico de uma variável é removida em tempo de execução.

Por exemplo:

List<String> lista = new ArrayList<String>();

O compilador “garante” que você só consiga adicionar objetos do tipo String dessa lista, mas quando sua classe é executada na JVM, não há qualquer verificação quanto ao tipo.

Sim, você pode dar um cast inseguro de List<String> para List e então adicionar outro tipo de objeto. O compilador vai aceitar, afinal ele “confia” em você.

É impossível para a instância lista saber o seu próprio tipo genérico, já que no fim das contas só existe uma única classe ArrayList, o tipo genérico é apenas um mecanismo de segurança para o desenvolvedor.

Li uma vez que manter o type satefy implicaria em um overhead muito grande para a máquina virtual, pois o Java precisaria praticamente criar uma cópia da classe List para cada tipo genérico usado no programa, de modo a ter literalmente uma versão da classe para cada tipo.

Quando é possível recuperar o tipo genérico

Repare no método getGenericSuperclass(). Ele retorna os tipos que a classe atual define para os parâmetros genéricos da superclasse imediata. Algo análogo ocorre com o getGenericInterfaces().

Isso significa que se você tiver uma subclasse assim:

class ClienteDao extends BaseDao<Cliente, Integer> { ... }

O código funcionaria perfeitamente, pois você está definindo um tipo para o parâmetro genérico da superclasse. Isso funciona porque o tipo genérico não está apenas numa variável, mas declarando de forma estática (fixa) na própria definição da classe.

Mas como você instancia diretamente o GenericDao, não há uma generic superclass e, portanto, a exceção sendo lançada.

Basta usar herança?

Usar sempre herança como no exemplo com extends é uma saída, mas muitos não recomendam porque há chances do código “quebrar” com novos erros estranhos.

Essa é uma possibilidade, pois como a própria documentação do método getGenericSuperclass() afirma, ele só retorna o tipo genérico implementado na superclasse imediata do seu DAO. Então se houver uma hierarquia diferente de exatamente dois níveis, o código vai falhar.

Recomendação

A recomendação que vai funcionar de forma mais simples e direta é passar a classe desejada por parâmetro para o construtor do GenericDao.

Exemplo:

public class GenericDao<T extends Serializable> {

    private final Class<T> entityType;

    public GenericDao(Class<T> entityType) {
        this.entityType = entityType;
    }

    ...

}    

Muito mais simples, certo?

O maior inconveniente é a chamada verbosa de criação da classe:

BaseDao<Cliente> clienteDao = new BaseDao<Cliente>(Cliente.class);

Na verdade, a partir do Java 7 poderíamos omitir o tipo Cliente da classe instanciada:

BaseDao<Cliente> clienteDao = new BaseDao<>(Cliente.class);

Melhorou um pouco, mas podemos fazer melhor. Que tal criar um método factory?

Exemplo:

public class BaseDao<T> {

    final private Class<T> entityType;

    public static <X> BaseDao<X> getInstance(Class<X> entityType) {
        return new BaseDao<X>(entityType);
    }

    private BaseDao(Class<T> entityType) {
        this.entityType= entityType;
    }

    ...

}

E a criação da instância da classe fica assim:

GenericDao<Cliente> clienteDao = BaseDao.getInstance(Cliente.class);

Alguns podem achar demais, porém considero este um design mais claro.

Considerações

Certamente há formas ainda mais flexíveis de trabalhar com genéricos usando Injeção de Dependência, Spring Beans e outros, mas por agora, estes fogem ao escopo do artigo.

O importante é compreender que Generics é um recurso importante e flexível da plataforma Java, com aplicações diretamente em nosso dia-a-dia.

Nuances sobre serialização de objetos com herança em Java

serialization

A serialização de objetos em Java é um recurso muito importante e útil em aplicações específicas. Diversas APIs do Java beneficiam-se dela, por exemplo, para chamadas de métodos remotos (RMI) e migração de sessões em Servlets de aplicações web clusterizadas.

Serializar um objeto consiste em converter os valores de uma instância em uma sequência (stream) de bytes (dados binários) de forma que o estado do objeto possa ser posteriormente recuperado.

Tornar uma classe serializável em Java é muito simples: basta implementar a interface java.io.Serializable.

Porém, nem sempre os detalhes são tão óbvios. E quanto a atributos herdados? O que ocorre se a superclasse não for serializável?

Tratando-se de herança, existem algumas nuances quanto ao que será ou não incluído na serialização. O ObjectOutputStream irá serializar todas as classes da hierarquia que são marcados com java.io.Serializable e seus descendentes. Desses, os atributos não estáticos, não transientes e que também são marcados com a referida interface serão serializados.

Meio complicado, não? Vamos ver um…

Exemplo prático (com erro)

Primeiro, duas classes que serão referenciadas, uma serializável e outra não:

class OutraClasseSerializavel implements Serializable {
    int outroValorSerializavel;
}

class OutraClasse {
    int outroValor;
}

Segundo, uma classe “pai” e uma “filha”:

class Pai {
    OutraClasse outraClassePai;
    OutraClasseSerializavel outraClasseSerializavelPai;
    int valorPai;
}

class Filha extends Pai implements Serializable {
    OutraClasse outraClasseFilha;
    OutraClasseSerializavel outraClasseSerializavelFilha;        
    int valorFilha;
}

Note que as duas classes possuem valores e referências para classes serializáveis e não serializáveis.

O que acontece se tentarmos serializar a classe Filha? Ocorre um java.io.NotSerializableException por causa da referência à classe não serializável OutraClasse na classe Filha.

Exemplo prático

Se removermos a referência à classe não serializável da classe Filha, o erro não ocorre:

class Filha extends Pai implements Serializable {
    OutraClasseSerializavel outraClasseSerializavelFilha;        
    int valorFilha;
}

Testando e analisando o resultado

Vamos fazer um teste:

Filha filha = new Filha();

//valores da classe filha
filha.valorFilha = 11;
filha.outraClasseSerializavelFilha = new OutraClasseSerializavel();
filha.outraClasseSerializavelFilha.outroValorSerializavel = 33;

//valores da classe pai
filha.valorPai = 22;
filha.outraClasseSerializavelPai = new OutraClasseSerializavel();
filha.outraClasseSerializavelPai.outroValorSerializavel = 44;
filha.outraClassePai = new OutraClasse();
filha.outraClassePai.outroValor = 55;

//serializa
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("filha.out")));
oos.writeObject(filha);
oos.close();

//recupera classe serializada
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("filha.out")));
Filha filhaRecuperada = (Filha) ois.readObject();
ois.close();

Finalmente, vamos imprimir e analisar os valores retornados…

Atributo primitivo na classe serializável

System.out.println(filhaRecuperada.valorFilha);

Saída:

11

Obviamente, o atributo valorFilha é devidamente serializado e recuperado porque faz parte da classe serializável e é um tipo primitivo.

Referência à classe serializável em uma classe também serializável

System.out.println(filhaRecuperada.outraClasseSerializavelFilha.outroValorSerializavel);

Saída:

33

O atributo outraClasseSerializavelFilha também foi serializado corretamente, assim como seu valor, porque é uma referência a uma classe serializável a partir da classe Filha que é serializável.

Atributo primitivo na classe Pai, que não é serializável

System.out.println(filhaRecuperada.valorPai);

Saída:

0

Observamos agora que, embora não ocorram erros, atributos estáticos em uma superclasse não serializável não são serializados.

Referência à classes serializáveis e não serializáveis em uma superclasse não serializável

System.out.println(filhaRecuperada.outraClassePai);
System.out.println(filhaRecuperada.outraClasseSerializavelPai);

Saída:

null

null

E, finalmente, observamos que referências a classes de qualquer tipo (serializáveis ou não) em uma superclasse não serializável também serão excluídas da serialização.

Considerações

Estender uma classe para torná-la serializável não funciona, pois como foi visto o processo de serialização ignora as superclasses não serializáveis e um erro ocorre ao incluirmos um atributo não serializável.

Mas existe alguma solução? A resposta é sim!

Solução: readObject e writeObject

A documentação da classe java.io.Serializable aponta alguns métodos que devem ser implementados para que você possa alterar “manualmente” a forma como o Java serializa e desserializa um objeto.

As assinaturas são:

private void writeObject(java.io.ObjectOutputStream out)
    throws IOException
private void readObject(java.io.ObjectInputStream in)
    throws IOException, ClassNotFoundException;

Exemplo de implementação

Segue uma implementação básica dos métodos readObject() e writeObject() na classe Filha que resolvem o problema da serialização tanto do atributo inteiro da superclasse quanto das referências a outros objetos:

class Filha extends Pai implements Serializable {

    int valorFilha;
    transient OutraClasse outraClasseFilha;
    OutraClasseSerializavel outraClasseSerializavelFilha;

    private void readObject(java.io.ObjectInputStream stream)
            throws IOException, ClassNotFoundException {
        valorFilha =  stream.readInt();
        outraClasseFilha = new OutraClasse();
        outraClasseFilha.outroValor = stream.readInt();
        outraClasseSerializavelFilha = (OutraClasseSerializavel) stream.readObject();

        valorPai = stream.readInt();
        outraClassePai = new OutraClasse();
        outraClassePai.outroValor = stream.readInt();
        outraClasseSerializavelPai = (OutraClasseSerializavel) stream.readObject();
    }

    private void writeObject(java.io.ObjectOutputStream stream)
            throws IOException {
        stream.writeInt(valorFilha);
        stream.writeInt(outraClasseFilha.outroValor);
        stream.writeObject(outraClasseSerializavelFilha);

        stream.writeInt(valorPai);
        stream.writeInt(outraClassePai.outroValor);
        stream.writeObject(outraClasseSerializavelPai);
    }

}

Então fazemos um novo teste:

Filha filha = new Filha();

//valores da classe filha
filha.valorFilha = 11;
filha.outraClasseSerializavelFilha = new OutraClasseSerializavel();
filha.outraClasseSerializavelFilha.outroValorSerializavel = 22;
filha.outraClasseFilha = new OutraClasse();
filha.outraClasseFilha.outroValor = 33;

//valores da classe pai
filha.valorPai = 44;
filha.outraClasseSerializavelPai = new OutraClasseSerializavel();
filha.outraClasseSerializavelPai.outroValorSerializavel = 55;
filha.outraClassePai = new OutraClasse();
filha.outraClassePai.outroValor = 66;

//serializa
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("c.out")));
oos.writeObject(filha);
oos.close();

//recupera classe serializada
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("c.out")));
Filha filhaRecuperada = (Filha) ois.readObject();
ois.close();

//valores da classe filha
System.out.println(filhaRecuperada.valorFilha);
System.out.println(filhaRecuperada.outraClasseSerializavelFilha.outroValorSerializavel);
System.out.println(filhaRecuperada.outraClasseFilha.outroValor);

//valores da classe pai
System.out.println(filhaRecuperada.valorPai);
System.out.println(filhaRecuperada.outraClasseSerializavelPai.outroValorSerializavel);
System.out.println(filhaRecuperada.outraClassePai.outroValor);

E obtemos a saída:

11
22
33
44
55
66

Todos os atributos foram salvos!

Conclusão

Embora o Java não resolva toda a questão da serialização automagicamente, ele nos fornece um mecanismo prático e flexível para resolver isso, pois permite controlar completamente como o objeto é salvo e recuperado do arquivo. Por outro lado, isso exige a codificação manual de cada elemento, na ordem correta.

Este artigo é baseado na minha resposta no Stack Overflow em Português

Créditos da imagem do sapo: GridGain Blog

Entenda definitivamente como usar o Apache POI: user model, shiftRows e clonagem de célula

Trabalhar com arquivos Excel é uma tarefa comum em empresas que fornecem software que gerenciam dados e valores de negócio. Mas nem sempre é fácil.

Há algum tempo publiquei um artigo sobre como adicionar segurança em arquivos do Excel com o POI. Também estou trabalhando num pequeno framework para gerar arquivos Excel com base em templates baseado no MVEL e no POI.

Deu pra notar que estou mexendo bastante com isso, né? Vamos a algumas dicas para quem está começando com o Apache POI.

User models – o que são e como usar

A primeira dica é algo que você deve saber. A biblioteca POI possui três diferentes APIs para trabalhar com arquivos Excel:

  1. HSSF: classes para trabalhar com o formado binário mais antigo do Excel (XLS).
  2. XSSF: classes para trabalhar com o formato mais novo do Excel baseado em XML (XLSX).
  3. SS: supermodelo que engloba tanto HSSF quanto XSSF.

Cada uma dessas APIs representa um user model, isto é, um modelo de dados para que o código “cliente” (o seu código) possa manipular um tipo específico de arquivo.

Ao trabalhar com classes do pacote hssf, você somente poderá ler e criar arquivos XLS. O pacote xssf somente lê e cria arquivos XLSX.

Enfim, use sempre as classes do pacote ss (supermodel) pois elas servem para os dois tipos de arquivos do Office. Caso contrário, você terá que fazer implementações distintas para trabalhar com arquivos binários e XML.

Desmistificando o método shiftRows

Uma das funcionalidades que implementei no desenvolvimento do framework foi a replicação automática de linhas. Também implementei um operador condicional para incluir ou não linhas no arquivo de acordo com variáveis. Para tanto, foi necessário implementar código para incluir e remover intervalores de linhas do arquivo.

Estudando a API, encontrei a função shiftRows da classe Sheet. Este método é frequentemente mal usado pelos programadores que não entendem bem como ele funciona.

Ok, a ideia do termo shift seria para deslocar as linhas. Mas o método possui os três parâmetros:

void shiftRows(int startRow, int endRow, int n)

Em geral, o entendimento inicial que as pessoas tem é que esse comando seria similar à funcionalidade do Excel de replicar linhas. Por exemplo, você seleciona as linhas de 1 a 3 e replica n vezes. Considere o comando:

sheet.shiftRows(0, 2, 5);

Será que esse comando copia as três primeiras linhas da planilha cinco vezes? A resposta é não.

Na verdade, o método shiftRows apenas desloca as linhas, alterando o número das linhas selecionadas. O comando acima simplesmente incrementa em 5 o número da linha das três primeiras linhas da planilha.

Após a chamada ao método, a linha 0 (primeira linha) irá para a posição 5 (sexta linha), a linha 1 irá para a posição 6 (sétima linha) e a linha 2 irá para a posição 7 (oitava linha).

Confuso? Pense da seguinte forma:

Pegue as linhas de 0 a 2 e mova cinco posições para “baixo”.

O mesmo raciocínio poderia ser aplicado caso o terceiro parâmetro fosse negativo. No caso, isso significa mover as linhas para “cima”. Enfim, este parâmetro diz em quanto as linhas selecionadas devem ser deslocadas, para “baixo” ou para “cima”.

Problemas com o shiftRows

É preciso tomar cuidado ao usar o método para não acabar com uma planilha corrompida. Isso já ocorreu várias vezes com várias pessoas que conheço.

O problema é que, ao deslocar as linhas com shiftRows, elas podem sobrepor outras linhas que já existem naquela posição. E isso pode causar vários comportamentos estranhos, desde a perda de alguns dados até uma planilha totalmente corrompida.

Imagine, no exemplo anterior, se já existissem as linhas 5, 6 ou 7.

Aparentemente, o POI não tem um tratamento para isso. Sinceramente, não verifiquei se isso foi corrigido em versões recentes, mas não é raro lidarmos com versões anteriores em projetos já existentes.

Portanto, sempre que for incluir linhas, mova todas as linhas até o final da planilha.

Exemplo:

sheet.shiftRows(
    posicaoParaInserir, 
    sheet.getLastRowNum(), 
    quantidadeLinhasNovas);

Para remover linhas, primeiramente é preciso remover manualmente os objetos do tipo Row existentes no intervalo, depois deslocar todas as linhas de baixo para cima.

Veja como ficou uma implementação minha para remover um intervalo de linhas:

private void removeRows(Sheet sheet, int position, int count) {
    for (int i = 0; i < count; i++) {
        Row row = sheet.getRow(position + i);
        if (row != null) {
            sheet.removeRow(row);
        }

    }
    sheet.shiftRows(position + count, sheet.getLastRowNum(), -count);
}

Explicando novamente, o código acima faz o seguinte:

  1. Remove as linhas existentes no intervalo começando em position até a quantidade count. Note que é necessário verificar se o objeto Row existe para cada linha, porque a implementação é de uma lista esparsa, isto é, as linhas que não tem informação não existem na memória.

  2. Desloca as linhas que estão acima do intervalo para cima, aplicando um valor negativo ao tamanho do intervalo. Em outras palavras, se eu quiser excluir as linhas 6 a 10, todas as linhas a partir da linha 11 terão o seu número subtraído de 5.

Clonar células

No Excel é simples duplicar o conteúdo com formatação. No POI esta tarefa demanda um certo trabalho. Não há um comando pronto, sendo necessário copiar o conteúdo, o estilo e alguns outros atributos de cada célula.

Fiz uma implementação baseada neste artigo do Stack Overflow:

private void cloneRow(Row sourceRow, Row newRow) {

    // Loop through source columns to add to new row
    for (Cell oldCell : sourceRow) {

        // Grab a copy of the old/new cell
        Cell newCell = newRow.createCell(oldCell.getColumnIndex());

        // Copy style from old cell and apply to new cell
        CellStyle newCellStyle = workbook.createCellStyle();
        newCellStyle.cloneStyleFrom(oldCell.getCellStyle());
        newCell.setCellStyle(newCellStyle);

        // If there is a cell comment, copy
        if (oldCell.getCellComment() != null) {
            newCell.setCellComment(oldCell.getCellComment());
        }

        // If there is a cell hyperlink, copy
        if (oldCell.getHyperlink() != null) {
            newCell.setHyperlink(oldCell.getHyperlink());
        }

        // Set the cell data type
        newCell.setCellType(oldCell.getCellType());

        // Set the cell data value
        switch (oldCell.getCellType()) {
            case Cell.CELL_TYPE_BLANK:
                newCell.setCellValue(oldCell.getStringCellValue());
                break;
            case Cell.CELL_TYPE_BOOLEAN:
                newCell.setCellValue(oldCell.getBooleanCellValue());
                break;
            case Cell.CELL_TYPE_ERROR:
                newCell.setCellErrorValue(oldCell.getErrorCellValue());
                break;
            case Cell.CELL_TYPE_FORMULA:
                newCell.setCellFormula(oldCell.getCellFormula());
                break;
            case Cell.CELL_TYPE_NUMERIC:
                newCell.setCellValue(oldCell.getNumericCellValue());
                break;
            case Cell.CELL_TYPE_STRING:
                newCell.setCellValue(oldCell.getRichStringCellValue());
                break;
        }
    }

    // If there are are any merged regions in the source row, copy to new row
    Sheet sheet = sourceRow.getSheet();
    for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
        CellRangeAddress cellRangeAddress = sheet.getMergedRegion(i);
        if (cellRangeAddress.getFirstRow() == sourceRow.getRowNum()) {
            CellRangeAddress newCellRangeAddress = new CellRangeAddress(
                    newRow.getRowNum(),
                    (newRow.getRowNum() +
                            (cellRangeAddress.getLastRow() - cellRangeAddress.getFirstRow())),
                    cellRangeAddress.getFirstColumn(),
                    cellRangeAddress.getLastColumn());
            sheet.addMergedRegion(newCellRangeAddress);
        }
    }

}

Considerações finais

A biblioteca POI é bem flexível e permite manipular arquivos de planilha de forma relativamente simples e eficiente.

Porém, determinadas funcionalidades podem representar um desafio até para o entendimento dela. Espero que este artigo, dentro dos cenários propostos, lhe possa ser útil. 😀

Conclusões sobre Estimação de Software

Este artigo é a conclusão da minha monografia de Especialização em Engenharia de Software. Espero que o possa ser útil de alguma forma para o leitor.

Introdução

Este capítulo apresenta as conclusões com base nos tópicos discutidos neste trabalho, relaciona possíveis trabalhos futuros que poderiam estender esta pesquisa e avalia a principal contribuição da mesma.

Conclusões

O primeiro capítulo estabeleceu algumas bases teóricas sobre o desenvolvimento de software com base na Engenharia de Software, uma disciplina que trata dos aspectos do desenvolvimento de software e provê abordagens para que o desenvolvimento atinja os objetivos do negócio.

O software não é fabricado em série como um produto “tradicional”, ele é desenvolvido. Tomando como exemplo um modelo de automóvel, o processo de fabricação do mesmo é definido linearmente do início ao fim. Ao invés disso, o desenvolvimento de software pode ser comparado ao projeto de criação do modelo do automóvel, o que ocorre antes da fabricação em série.

A arquitetura de um software é importante para a estimação, pois ela determina os tipos de componentes que irão compor o software e os relacionamentos entre eles, afetando diretamente a complexidade e as regras de desenvolvimento.

Existem barreiras no desenvolvimento e dificuldades intrínsecas na estimação de software. Algumas delas tem sua origem em características do software como intangibilidade, complexidade e mutabilidade. Outras são oriundas de fatores humanos, como a dualidade de visão entre desenvolvedores, que pensam em nível funcional, e gerentes, que muitas vezes são orientados a cronograma e custos.

O segundo capítulo apresentou noções sobre Engenharia de Requisitos, uma atividade que busca o entendimento do software a ser construído e o gerenciamento das mudanças no decorrer do desenvolvimento.

Requisitos podem ser representados em vários níveis de detalhamento, mas para o desenvolvimento de software é necessário que eles sejam definições matematicamente formais das funcionalidades do software. Requisitos não existem por si só, isto é, eles não são levantados, encontrados ou identificados, mas são elicitados e definidos por um analista.

Requisitos são fundamentais para a estimação, pois fornecem a abstração necessária do problema para que os engenheiros de software cheguem ao modelo da solução que represente o instantâneo dos requisitos num determinado momento.

Estimativas são derivadas do modelo de análise e design. Estes modelos representam uma abstração da solução a ser desenvolvida em forma de software.

Ainda que algumas abordagens de estimação não incluam um modelo formal, os estimadores acabam usando um modelo de análise implícito em suas mentes, pois para chegar a uma estimativa de desenvolvimento eles precisam mentalizar a arquitetura e os componentes a serem desenvolvidos.

Os requisitos mudam ao longo do tempo por definição. O software é construído com base num modelo que representa os requisitos elicitados num determinado momento.

As mudanças nos requisitos devem ser gerenciadas para que seja possível identificar o impacto no software. A análise de impacto pode se beneficiar da rastreabilidade, isto é, uma relação entre os componentes do software que permita identificar quais deles devem ser modificados para atender uma mudança de requisito. Neste ponto, a arquitetura exerce um papel fundamental ao estabelecer a forma de organização e comunicação entre os componentes.

A qualidade da estimação no decorrer do desenvolvimento depende do gerenciamento de mudança dos requisitos, da qualidade da arquitetura e da rastreabilidade para que o estimador tenha consciência das alterações que são necessárias no software. Sem isso, é provável que o desenvolvimento siga sem um horizonte visível.

O terceiro capítulo discorreu sobre processos de desenvolvimento e gerenciamento de software. Processos são fundamentais para organizar o desenvolvimento de software e evitar o caos. Todo desenvolvimento segue um processo, seja ele bem definido, informal ou empírico.

Os modelos de processo propostos pela Engenharia de Software são abstrações de processos que as organizações podem implementar em seus projetos.

Alguns processos focam mais o gerenciamento do projeto, enquanto outros o desenvolvimento do software. Por isso, processos podem ser usados de forma complementar em um projeto de desenvolvimento de software.

Os processos podem ser classificados de acordo com a burocracia ou disciplina exigida. Processos burocráticos facilitam o gerenciamento, mas acrescentam uma carga adicional aos envolvidos no projeto. Processos empíricos, como os processos ágeis, procuram eliminar essa carga, baseando-se na confiança e interação entre a equipe ao invés da formalização.

A estimação de software deve considerar o processo adotado. Estimativas devem ser ajustadas de acordo com as atividades definidas e os produtos de trabalho exigidos pelo processo vigente, pois estes elementos impactam na produtividade da equipe. Além disso, estimativas de prazo e cronograma podem ser ajustadas e atualizadas de acordo com a iteratividade do processo e do modo como as funcionalidades são implementadas, isto é, num modelo evolucionário ou incremental.

Os processos ágeis introduziram uma séria de conceitos ao desenvolvimento de software procurando ser bastante adaptáveis às mudanças de requisitos, minimizar o tempo de resposta a estas mudanças e, dessa forma, atender as necessidades mais imediatas dos clientes. Para atingir esses objetivos, o processo é reduzido a um mínimo de disciplina e organização e espera-se que a equipe de desenvolvimento esteja motivada e se auto-organize para atender as necessidades imediatas do projeto.

Na prática, várias atribuições do gerente do projeto recaem sobre a equipe de desenvolvimento. Quando esta equipe possui maturidade e habilidade suficiente para arcar com essas responsabilidades, o que inclui planejar e estimar suas próprias tarefas, o processo ágil realmente pode otimizar o desenvolvimento software. Em contrapartida, o desenvolvimento pode chegar ao caos se os indivíduos envolvidos não corresponderem a estas expectativas.

Em decorrência disso, mesmo os “agilistas” às vezes utilizam certas práticas contrárias ao modelo ágil, como em casos onde o responsável pelo projeto ajusta as estimativas da equipe.

O quarto capítulo apresentou noções de estimação de software e algumas das técnicas comumente utilizadas em processos tradicionais e ágeis.

Estimativas são importantes do ponto de vista gerencial, pois clientes e gerentes precisam ter um horizonte sobre o desenvolvimento de um software.

O objetivo da estimação não é simplesmente fornecer uma data de entrega, mas possibilitar decisões corretas de negócio para que os objetivos da organização sejam atingidos ou ainda a correta avaliação desses objetivos para averiguar se eles são realmente tangíveis.

Estimativas não devem ser precisas, ou seja, utilizar uma unidade de medida pequena ou casas decimais, elas precisam ter acurácia, que significa estar próximo à realidade.

Embora estimativas geralmente sejam representadas numericamente, elas representam uma ordem de grandeza e não valores reais e absolutos. Manipular matematicamente os números das estimativas é um erro e leva à falsa sensação acurácia. Por exemplo, dobrar a quantidade de programadores não diminui pela metade o tempo de codificação. Esta e outras armadilhas são evitadas quando se entende a natureza real da estimação, que não consiste em medir algo, mas tentar prever sua ordem de grandeza.

Além disso, uma estimativa não deve ser confundida com o compromisso da equipe de desenvolvimento em concluir o trabalho num determinado prazo, tampouco com o plano da organização para atingir uma meta. É um erro grave manipular estimativas para adequá-las a uma determinada agenda. Cabe ao gerente do projeto usar as estimativas para calcular os recursos necessários para cumprir um plano ou então negociar os prazos do projeto.

Estimativas possuem um grau de incerteza. No de correr do projeto, quanto mais cedo a estimação for realizada, maior a incerteza. Em teoria, à medida que o desenvolvimento evolui, a incerteza diminui proporcionalmente ao conhecimento mais detalhado sobre o problema e a solução. Entretanto, isto somente será a realidade se houver o gerenciamento adequado, caso contrário o desenvolvimento pode acabar num ciclo interminável de correções e mudanças.

Bons estimadores devem considerar influências externas sobre o projeto e o software. Isto inclui fatores humanos, do ambiente do projeto, políticos e técnicos. Certas características do projeto também devem ser levadas em conta, como o tamanho do projeto, tipo de software, experiência da equipe e restrições tecnológicas.

Além disso, a qualidade da estimação é afetada por diversos fatores, tais como falha na elicitação de requisitos, falta de experiência da equipe, atividades omitidas, otimismo sem fundamento dos desenvolvedores e tendências pessoais do estimador.

Embora muitas variáveis influenciem na estimação, não é recomendável investir esforço demasiado nesta atividade, assim como se deve evitar exagero de detalhes. Diversos autores verificaram que, a partir de um determinado ponto, esforços e detalhes adicionais desviam as estimativas da realidade.

O mesmo princípio é aplicado à precisão desnecessária. Estimar um projeto de vários meses em horas de trabalho, embora seja uma unidade precisa, passa uma falsa sensação de acurácia. Neste caso, seria melhor adotar uma unidade de dias ou semanas de trabalho.

Diferentes técnicas de estimação foram apresentadas. Em geral, elas consistem em contar e julgar os elementos da solução proposta, em seguida aplicar uma fórmula prescrita e, finalmente, calibrar os resultados obtidos através de dados históricos e da avaliação das influências externas e características do projeto. A arquitetura do software é importante para a identificação e classificação dos elementos do sistema.

Algumas técnicas de estimação utilizam elementos do problema ao invés da solução, tal como o Planning Poker. Neste caso, o estimador acaba por construir mentalmente a ponte de ligação entre as necessidades dos clientes e da solução proposta, isto é, o modelo de análise e design.

Outras técnicas, como o Function Point, utilizam elementos pré-definidos que não levam em consideração a arquitetura do software. Técnicas deste tipo podem levar a desvios na estimação, pois nem sempre um software pode ser representado adequadamente no modelo exigido pela técnica.

A qualidade da estimação depende diretamente da habilidade, do conhecimento e da experiência do estimador. Em geral, o gerente do projeto é responsável pela estimação. Ele pode delegar esta atividade como um todo ou em parte para um ou mais membros da equipe, como nos processos ágeis. Porém, é necessário avaliar se as pessoas envolvidas estão capacitadas, afinal, é um consenso que desenvolvedores em geral são muito otimistas quando fornecem estimativas.

Alguns autores consideram a estimação como uma arte, outros como um processo de engenharia. Ambos contam com certa razão, pois tanto um bom artista como um bom engenheiro precisam de treino, técnica e habilidade para exercer seus talentos com perfeição. Além disso, o próprio desenvolvimento do software é considerado por alguns como um processo artesanal. No entanto, embora o talento dos indivíduos seja um elemento importante no desenvolvimento e na estimação, o conhecimento e o uso de técnicas adequadas irá potencializar esse talento.

Este estudo pretendia incluir uma comparação entre as técnicas de estimação com a finalidade de identificar as melhores técnicas disponíveis. Mas estimativas são previsões. Não há como comparar objetivamente técnicas que procuram antecipar o futuro.

A escolha da técnica de estimação em um projeto depende do que o estimador considerar conveniente. Assim, este estudo buscou trazer conhecimentos essenciais sobre estimação e os elementos que mais o influenciam para que o estimador tenha uma visão mais ampla sobre esta atividade e tome decisões conscientes.

O estimador deve conhecer o processo de desenvolvimento o mais detalhadamente e abrangentemente possível, ser realista e sincero quanto ao tempo de cada atividade e saber negociar os prazos com a área de negócios, que sempre exigirá entregas antecipadas.

Trabalhos Futuros

Como base teórica, o presente estudo limitou-se a apresentar conceitos sobre aplicativos de software, engenharia de requisitos, processos de desenvolvimento e gerenciamento de software que influenciam na estimação de software. Além disso, conceitos de estimação e diversas técnicas foram apresentados, concluindo com um estudo de caso com a aplicação de uma delas.

Outros trabalhos podem ser realizados comparando como as técnicas de estimação se encaixam nos diversos processos existentes. Algumas técnicas necessitam de determinados modelos que não são recomendados em processos ágeis, por exemplo. Por outro lado, o Planning Poker pode ser de difícil de execução quando requisitos formais estão especificados e não há estórias de usuário.

Outra possibilidade de estudo complementar seria analisar a usabilidade das técnicas de estimação, ou seja, verificar na prática se as técnicas são usadas corretamente ou se elas podem levar a armadilhas. Uma técnica pode se apresentar boa em teoria, mas a falha em entender a técnica pode levar a estimativas equivocadas.

Além disso, uma análise mais detalhada das técnicas usadas em processos ágeis poderia ser útil para verificar se essas técnicas trazem algum benefício em relação às técnicas mais tradicionais. Poderia-se analisar como esses métodos percorrem a distância entre o problema a solução para fazer a estimação ou ainda analisar as contradições e os pontos em que os “agilistas” discordam entre si.

Página 5 de 16

Creative Commons O blog State of the Art de Luiz Ricardo é licenciado sob uma Licença Creative Commons. Copie, compartihe e modifique, apenas cite a fonte.