Categoria: Desenvolvimento (Página 2 de 8)

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)

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.

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.

Qual a diferença entre MVC action based e component based?

Frameworks Component Based

Frameworks Component Based mantém sincronia entre os estados dos componentes da view e do seu modelo de dados no lado do servidor.

Quando o usuário interage com a tela, as alterações realizadas são, em um dado momento, refletidas no modelo que fica no servidor.

No JSF, por exemplo, a “tela” é gerada por um facelet, que nada mais é que um XML que define quais componentes serão exibidos para o usuário e associam os valores desses componentes a um objeto (Java Bean) que fica no servidor. Esses componentes são então renderizados em HTML e, quando o usuário executa uma ação, o JSF atualiza os objetos no servidor.

Não encontrei uma representação visual adequada, mas algo aproximado num artigo da Caelum sobre o tema:

Component Based Request

Component Based Request

Em frameworks component based, a view é responsável por mapear valores para os beans e para o modelo. A imagem acima ilustra a ordem de chamadas:

  1. O usuário executa uma ação no sistema
  2. O front controller do framework atualiza os componentes da view com o estado atual
  3. O método do Managed Bean é chamado (usando JSF como exemplo), podendo executar alguma regra de negócio com os novos valores
  4. Finalmente, o modelo do sistema é atualizado

Frameworks Action Based

Já os frameworks Action Based não mantém necessariamente esse vínculo entre os estados do servidor e do cliente.

Isso não quer dizer que o desenvolvedor não possa armazenar estado no servidor, por exemplo, na sessão do usuário, mas que o vínculo entre o modelo e a view não é tão acoplado como no modelo Component Based.

Um framework Action Based geralmente irá receber diretamente requisições HTTP. Isso torna o modelo action based mais flexível, já que o desenvolvedor pode optar por qualquer tipo de view que gere uma requisição HTTP compatível.

Considere a ilustração a seguir (da mesma fonte anterior):

Action Based Request

Action Based Request

O resumo dos passos da execução é:

  1. O usuário executa uma ação no sistema
  2. O front controller do framework direciona a requisição e os parâmetros para um método do controller
  3. O controller lê os parâmetros necessários e executa regras de negócio que atualizam o modelo
  4. O controller “devolve” uma view para o usuário

Conclusão

Podemos dizer que os frameworks component based são mais centrados nas views (com seus componentes que mapeiam o modelo e os dados do usuário), enquanto os action based são mais centrados nos controllers (que recebem parâmetros via request).


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

Estimando Software: um Estudo de Caso

Este estudo de caso é o Capítulo V da minha monografia de Especialização em Engenharia de Software. Espero que possa ser útil de alguma forma para o leitor.

Introdução

Este capítulo apresenta um estudo de caso prático de estimação num contexto comum de um projeto de desenvolvimento de software em uma empresa a fim de ilustrar a aplicação dos conceitos apresentados neste trabalho.

Contexto

Uma empresa de desenvolvimento de software trabalha primariamente com softwares “de prateleira” (Commercial Off-The-Shelf – COTS) e faz adaptações sob demanda para seus diversos clientes no segmento financeiro. Um projeto de desenvolvimento pode ser criado quando se identifica a necessidade de um novo sistema ou a partir da solicitação de clientes para implementação de novas funcionalidades ou modificações nas já existentes.

Os sistemas são desenvolvimentos na plataforma Java com arquitetura web, onde o usuário acessa através de um navegador web. A arquitetura dos sistemas adota o padrão Model-View-Controller (MVC). O model (modelo) consiste em um conjunto de componentes que disponibiliza serviços que realizam cálculos, atualizam as entidades do sistema e fazem integrações com outros sistemas. A view (visão) consiste num conjunto de tecnologias para geração de interfaces web exibidas no navegador do usuário a partir dos dados do model. O controller (controlador) consiste em um componente que recebe ações e dados do usuário, atualiza o model e redireciona o usuário para a view adequada.

No início de um projeto, a elicitação dos requisitos é realizada por um analista de negócios, que cria o artefato chamado de Especificação de Requisitos de Software (ERS). A ERS pode conter, além dos requisitos, o design da solução. O design pode ser feito pelo analista de negócios quando este entende da solução ou por um Analista de Sistemas da equipe. Então o gerente do projeto, cujo papel é geralmente desempenhado pelo coordenador da equipe que irá desenvolver o software, faz o planejamento e a estimação inicial.

No planejamento, o gerente cria um cronograma, que pode um gráfico de Gantt contendo as atividades de desenvolvimento e testes para cada funcionalidade da ERS. A partir dessas informações é possível negociar o prazo com o cliente e alocar a equipe de acordo com o esforço necessário.

O processo de desenvolvimento da empresa pode ser no modelo Waterfall ou iterativo, dependendo do tamanho do projeto e da necessidade do cliente. Porém, o desenvolvimento não é incremental, sendo a implementação das funcionalidades simplesmente distribuída sequencialmente pelo projeto.

Detalhes do projeto

Uma nova funcionalidade de “estorno de aditamento” de contrato financeiro foi solicitada em um dos sistemas da empresa. Um novo projeto foi iniciado para tratar esta demanda.

Um aditamento consiste em uma alteração contratual realizada a partir do segundo mês de vigência do contrato, pois alterações dentro do mês de entrada não caracteriza um aditamento.

Um estorno de aditamento consiste em desfazer as alterações realizadas no processo de aditamento considerando todos os impactos nas integrações com outros sistemas e restaurando todos os aspectos originais do contrato antes do aditamento.

O diagrama a seguir ilustra os casos de uso relacionados a aditamento:

Figura 12 – Casos de Uso do projeto do Estudo de Caso

Figura 12 – Casos de Uso do projeto do Estudo de Caso

Ambos os conceitos são de domínio da equipe, pois funcionalidades semelhantes já foram implementadas diversas vezes, porém sem registros de tempo das atividades realizadas, de modo que não há dados históricos para a estimação no projeto atual.

Como há somente uma funcionalidade no projeto, o desenvolvimento seguirá o modelo Waterfall. Os requisitos serão elicitados, depois serão feitos o design e o planejamento de todo o projeto, seguidos pela estimação do esforço. Após o desenvolvimento da nova funcionalidade, uma versão executável do sistema será enviada para testes e então disponibilizada para o usuário final.

Correções serão realizadas durante a fase de testes internos da empresa e também, quando problemas ocultos forem encontrados durante os testes de aceitação dos usuários finais do sistema.

Análise e Design da Solução

O estorno de aditamento consiste em desfazer o aditamento de um contrato, portanto foi realizado um levantamento da funcionalidade de aditamento já existente e verificou-se que ela afeta vinte e duas entidades do sistema cujos dados são armazenados em tabelas no banco de dados. Logo, o estorno deverá restaurar o estado de todas essas tabelas. O diagrama de sequência abaixo ilustra, em alto nível, o processo de estorno de aditamento:

Figura 13 – Diagrama de Sequência da funcionalidade de Estorno de Aditamento

Figura 13 – Diagrama de Sequência da funcionalidade de Estorno de Aditamento

A fim de restaurar o estado anterior de uma entidade é necessário que exista um histórico de alterações. Algumas entidades já possuem tal histórico, enquanto doze delas não. Entretanto, as entidades que não possuem informações históricas não serão modificadas para minimizar o impacto nas demais funcionalidades do sistema. Tabelas de dados históricos auxiliares serão criadas, contendo os mesmos atributos de entidade e a data em que ocorreu a alteração. A funcionalidade de aditamento será alterada de modo que os dados anteriores de todas as entidades modificados sejam armazenados nessas tabelas de dados históricos.

No aditamento, uma interface de integração com um sistema de contabilidade é acionada através de serviços de componentes de software. No estorno do aditamento, deve ser acionado o serviço correspondente para gerar o efeito contrário do aditamento anterior.

Além disso, conforme o padrão dos sistemas da empresa, esse tipo de funcionalidade contará com duas interfaces com o usuário. A interface de pesquisa de contratos listará os contratos que foram aditados, contendo alguns filtros e uma tabela com o resultado, a qual permitirá selecionar um dos contratos para a realização do estorno. A interface de confirmação do estorno exibirá os dados do contrato após a seleção de um dos contratos na pesquisa e permitirá a confirmação do estorno do aditamento.

Os componentes do model do sistema deverão disponibilizar o serviço de pesquisa dos contratos aditados com os devidos filtros e o serviço principal que efetuará o estorno do aditamento do contrato selecionado. Os serviços serão acionados pelos controllers, que, por sua vez, acionarão as views para exibir as interfaces do usuário.

O cenário de uso básico do sistema está representado no Diagrama de Atividades abaixo:

Figura 14 – Diagrama de Atividades da funcionalidade de Estorno de Aditamento

Figura 14 – Diagrama de Atividades da funcionalidade de Estorno de Aditamento

Estimação através da técnica de Pontos de Função

Através dos dados da análise e do design da funcionalidade de estorno de aditamento, obteve-se a tabela abaixo, a qual contém as entradas necessárias para a técnica de estimação por pontos de função:

Tabela 7 – Tabela com contagem de Pontos de Função do Estudo de Caso

Valor do Domínio Contagem Simples Médio Complexo
Entradas Internas 2 x 3 4 6 = 6
Saídas Externas 2 x 4 5 7 = 8
Consultas Externas 1 x 3 4 6 = 6
Arquivos Lógicos Internos 1 x 7 10 15 = 15
Arquivos de Interface Externa 0 x 5 7 10 = 0
Contagem Total 35

Existem duas Entradas Internas, que são as duas interfaces com o usuário, pois tanto na pesquisa quanto na tela de confirmação o usuário deve interagir com a tela. As duas interfaces também são Saídas Externas, pois exibem dados dos contratos aditados. Elas são consideradas simples por possuírem poucos dados de entrada. Além disso, existe uma Consulta Externa, que consiste na integração com o sistema de contabilidade através do acionamento de um serviço de retorno imediato, a qual é considerada complexa por possuir mais um conjunto de dados com mais de quinze elementos. Além disso, existe um Arquivo Lógico Interno, isto é, apenas um conjunto de dados identificável pelo usuário. Apesar de vinte e duas tabelas ordinárias serem afetadas, mais as doze tabelas de dados históricos que deverão ser criadas, as diversas rotinas CRUD reutilizadas e criadas não acrescentam complexidade. Por último, não existem Arquivos de Interface Externa.

Os seguintes fatores de ajuste (Fi) foram identificados:

  • item 4, desempenho crítico, com valor 3;
  • item 5, ambiente intensamente utilizado, com valor 2;
  • item 10, processamento complexo, com valor 5.

Aplicando os valores obtidos na equação da técnica de estimação utilizada, o valor total de pontos de função resultante foi de 26,25.

É necessário multiplicar os pontos de função por um fator de horas por ponto de função de modo a estimar o esforço. Embora não haja dados históricos para apoiar a estimação no projeto atual, é possível estimar com base em dados médios registrados por organizações reconhecidas.

O Brazilian Function Point Users Group (BFPUG, 2013) apresenta diversas medidas de horas por ponto de função. Para a plataforma Java, as médias variam de 10 a 60 horas por pontos de função. Considerando a estimativa mais otimista, a funcionalidade de estorno de aditamento demandaria cerca de 260 horas de trabalho ou, utilizando como base o número de 160 horas de trabalho em um mês, o desenvolvimento levaria pouco mais de um mês e meio, contando com um desenvolvedor.

Com base no total de pontos de função estimados, é possível ainda derivar a estimação do tamanho do software em linhas de código (LOC). Pressman (2006, p. 505) apresenta uma tabela onde a linguagem Java possui uma média de 63 linhas por pontos de função. Assim, pode-se estimar o desenvolvimento de 1.653,75 linhas de código para a funcionalidade de estorno de aditamento.

Análise dos resultados

A estimativa de esforço obtida através da técnica de estimação por pontos de função é razoável quando comparada com o julgamento de um especialista, isto é, alguém com conhecimento sobre o software e o requisito em questão.

Entretanto, caso o desenvolvimento da funcionalidade do estudo de caso fosse realizado por um desenvolvedor com pouca experiência ou pouco conhecimento do software, a estimação deveria ser feita com base num número de horas por ponto de função mais pessimista.

Conclusão

O estudo de caso ilustra uma situação comum das empresas de desenvolvimento de software e exemplifica a aplicação de uma técnica de estimação de software.

A análise crítica do resultado de uma estimação é importante para evitar que equívocos no detalhamento das funcionalidades gerem grandes distorções. Caso o resultado gere desconfiança no estimador, deve-se revisar cada passo da estimação.

Além disso, embora coeficientes genéricos como a média de horas por pontos de função possam ser utilizados inicialmente, é essencial registrar os dados do desenvolvimento para uso em estimações futuras a fim de se obter um coeficiente mais adequado para o cálculo de esforço, que resulte em estimativas mais próximas à realidade no contexto do projeto e da equipe.

Página 2 de 8

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.