novembro 18, 2010

Mais polimorfismo, menos estruturas de controle...

Programadores OO estão acostumados com polimorfismo, porém, programadores da escola procedural ou programadores que estão iniciando no mundo OO, não conhecem ou não enxergam a utilidade prática deste recurso.

A definição do Deitel para polimorfismo é:

"O polimorfismo, permite 'programar no geral' em vez de 'programar no específico'. Em particular, o polimorfismo permite escrever programas que processam objetos que compartilham a mesma superclasse em uma hierarquia de classes como se todas fossem objetos da superclasse."

Mas como isso funciona na prática?

Mesmo com uma linguagem orientada a objetos como o JAVA, é possível escrever programas procedurais. Orientação a objetos está muito mais ligada à decisões de design de software, do que simplesmente à utilização de uma linguagem orientada a objetos.

Por exemplo, usando JAVA, poderíamos escrever o seguinte código:

public class TestAnimais {
 
     enum TipoAnimal{CACHORRO, GATO, GALINHA};
 
     public static void main(String[] args) {
  
          TipoAnimal tipo = TipoAnimal.CACHORRO;
          Animal a = new Animal(tipo);
  
          switch (a.getTipo()) {
               case CACHORRO:
                    System.out.println("ROOF");
                    break;
               case GATO:
                    System.out.println("MEOW");
                    break;
               case GALINHA:
                    System.out.println("CÓ-CÓ-CÓ");
                    break; 
               default:
                    break;
          }
    }
}

Este trecho de código é tipicamente procedural. Programas desenvolvidos utilizando este paradigma, costumam ter muitas instruções de controle do tipo if e switch, para tomada de decisões em tempo de execução.

Podemos refatorar o código acima utilizando uma abordagem polimórfica, orientada a objetos, para reduzir o uso de estruturas de controle (menos 'código espaguete') e tornar o design mais extensível.

Primeiro, criamos uma classe abstrata Animal:

public abstract class Animal { 
     public abstract void emitirSom();
}

Em seguida, criamos uma classe concreta Cachorro, que estende Animal.

public class Cachorro extends Animal {

     public void emitirSom() {
          System.out.println("ROOF");
     }
}

Com este design, nossa 'classe cliente' poderia ficar assim:

public class TestAnimais {
 
     public static void main(String[] args) {
          Animal a = new Cachorro();
          new TestAnimais().digaAlgo(a);
     }
 
     // método ilustrativo
     public void digaAlgo(Animal animal){
          // método polimórfico (Qual animal?)
          animal.emitirSom();
     }
}

O polimorfismo se manifesta no momento em que fazemos uma variável de referência da superclasse (Animal), apontar para um objeto da subclasse (Cachorro) na heap. Desta forma, quando ordenamos que um animal emita som, o método emitirSom() utilizado, é resolvido em tempo de execução baseado no tipo do objeto na heap, sem a necessidade de testes condicionais.

Muitos argumentam que programar orientado a objetos é mais lento. De fato, a curto prazo, esta afirmação muitas vezes se mostra verdadeira.

Os grandes benefícios do paradigma aparecem a médio e longo prazo, principalmente na fase de manutenção do projeto (onde se gasta aproximadamente 80% do tempo).

Por exemplo, se quisermos adicionar um novo animal ao projeto, basta criar um classe concreta (ex.: Gato) que estenda Animal, pois não há risco de comprometer a integridade do que já funciona (design extensível), além do fato de que qualquer método que receba ou retorne um objeto do tipo Animal (ex.: public void digaAlgo(Animal animal)), pode trabalhar normalmente com a nova classe concreta, sem a necessidade de adição de código extra, graças ao polimorfismo.

setembro 25, 2010

Trabalhando com JPA, fora de um container Java EE.

Resolvi escrever este post, para compartilhar a minha experiência com a utilização da API de Persistência do JAVA (JPA), fora de um container Java EE (ex.: TOMCAT). No meu caso, o ambiente em questão é composto por: Tomcat, Hibernate e PostgreSQL.

Ao utilizar a JPA em um ambiente "não Java EE", o desenvolvedor não pode contar com o recurso de "Dependency Injection" (pelo menos não, na camada de modelo) e deve, por conta própria, gerenciar o ciclo de vida dos Entity Managers no domain model. Não realizar esta tarefa de forma eficiente, pode resultar em situações desagradáveis, como o travamento do banco de dados por conexões abertas em excesso.

A interface EntityManager, faz a "ponte" entre o mundo orientado a objetos e o mundo relacional,  ela oferece serviços de persistência para objetos, como não é possível contar com a Dependency Injection,  é necessário utilizar a interface EntityManagerFactory, que também faz parte da API, para obter um EntityManager

Entity Managers obtidos através de uma Entity Manager Factory, são conhecidos como Applicaton-Managed Entity Managers, é de responsabilidade do desenvolvedor gerenciar seu ciclo de vida, os que são gerenciados pelo container, são conhecidos como Container-managed Entity Managers.

Como EntityManagerFactory é um objeto de carga pesada, uma idéia básica é, criar uma única factory, através de um Singleton e permitir acesso à ela de forma global. Assim, as classes responsáveis por realizar a comunicação com o banco de dados, podem obter objetos do tipo EntityManager, através desta factory compartilhada, sempre que necessário.

Uma abordagem para alcançar este objetivo envolve: 
  1. A Criação de um Listener que verifica se a aplicação foi iniciada ou finalizada;
  2. A Criação de um singleton responsável pela factory
  3. Gerenciar ciclo de vida de Entity Managers no escopo dos métodos que fazem acesso à banco de dados.
1. O Listener

public class PersistenceAppListener implements ServletContextListener {
  
     public void contextInitialized(ServletContextEvent evt) {
     }


     public void contextDestroyed(ServletContextEvent evt) {
          PersistenceManager.getInstance().closeEntityManagerFactory();
     }
}


precisamos adicioná-lo ao web.xml:

<description>ServletContextListener</description>
<listener>
     <listener-class>my.package.PersistenceAppListener</listener-class>
</listener>

A idéia do listener é observar se a aplicação foi finalizada, para liberar os recursos.


2. O Singleton

public class PersistenceManager {
 
 public static final boolean DEBUG = true;
   
   private static final PersistenceManager singleton = 
                                             new PersistenceManager();
   
   protected EntityManagerFactory emf;
   
   public static PersistenceManager getInstance() {
     
     return singleton;
   }
   
   private PersistenceManager() {
   }
  
   public EntityManagerFactory getEntityManagerFactory() {
     
     if (emf == null)
       createEntityManagerFactory();
     return emf;
   }
   
   public void closeEntityManagerFactory() {
     
     if (emf != null) {
       emf.close();
       emf = null;
       if (DEBUG)
         System.out.println("Persistence finished at " + 
                                       new java.util.Date());
     }
   }
   
   protected void createEntityManagerFactory() {
     
     this.emf = Persistence.createEntityManagerFactory("MyUnit");
     if (DEBUG)
       System.out.println("Persistence started at " + 
                                 new java.util.Date());
   }
}

O Singleton é responsável por fornecer a factory global através do método getEntityManagerFactory().

3. Utilizando Entity Managers

Segue o exemplo de código cliente, que utiliza a factory para obter um EntityManager

public class MyRepository  {
     
     // variável de instância 
     private EntityManagerFactory emf = PersistenceManager.getInstance().getEntityManagerFactory();
     
     // Insere objeto no banco
     public void add(myObject object) {
      
          EntityManager em = this.emf.createEntityManager();
          try{
               EntityTransaction et = em.getTransaction();
               try {
                    et.begin();
                    em.persist(object);
                    et.commit();
               } finally {
                    if (et.isActive()) et.rollback();
               }
          } finally {
               em.close();
          }  
     }
}

No geral, esta abordagem atendeu bem às minhas necessidades, mas eu enxerguei duas desvantagens neste modelo:
  1. A dificuldade de utilizar LAZY LOAD de objetos, pois o ciclo de vida dos Entity Managers têm escopo de método. Os relacionamentos @OneToMany e @ManyToMany, utilizam lazy load por default. Uma forma de resolver este problema é anotar forçando EAGER LOADING, ex.: @OneToMany(fetch=FetchType.EAGER)
  2. A quantidade de código extra (try's aninhados), necessário para manter a gerência de ciclo de vida dos Entity Managers, de forma eficiente.
Para saber mais: 

agosto 26, 2010

Agile

Ontem assisti a uma palestra sobre desenvolvimento de software chamada: "Desenvolvendo software com qualidade". Dentre os assuntos abordados, o palestrante falou sobre RUP e XP.

Eu não concordei com muitas das idéias passadas pelo palestrante, sobre desenvolvimento ágil, em especial, não concordei com dois tópicos colocados por ele:

  1. Metodologia ágil é só para projetos pequenos;
  2. Metodologia ágil gera código de baixa reusabilidade.
Na minha opinião, processos mais tradicionais como o RUP, são mais cômodos, pois são cartilhas a serem seguidas, desenvolvimento ágil requer mais disciplina. 

Disciplina é uma palavra que vejo sempre associada à métodos ágeis, isso por vezes, tira os envolvidos da sua zona de conforto.

Com o arcabouço oferecido pelo Agile, acredito que uma equipe consiga lidar com projetos de grande porte e se suportada pelo DDD, a questão da reusabilidade não será problema.

Compartilhei minha opinião com o mestre Vinícius e ele escreveu um excelente post sobre o assunto.

agosto 16, 2010

É só um 'cadastrinho'...

O processo de desenvolvimento de software é uma via de mão dupla: a equipe de desenvolvimento e o(s) domain expert(s) aprendem sobre o domínio em questão.

A equipe de desenvolvimento aprende, na maioria das vezes, sobre uma área de atuação completamente nova e o domain expert, ou a equipe de domain experts, descobrem novas funcionalidades que a principio, não haviam sido identificadas, é um processo evolutivo.

Eu diria que é impossível, o cliente ter uma idéia 100% correta com relação às funcionalidades a serem implementadas em um sistema. Alguns têm uma noção muito boa, com esses, o trabalho normalmente flui muito bem, mas muitos, têm uma noção bem distante da realidade de seus domínios.

Nesse contexto, é comum surgirem os 'cadastrinhos', como eu costumo chamar. O domain expert, por não ter uma noção muito correta do que precisa, ou por querer de certa forma, minimizar a complexidade do próprio processo, diz: "Esse sistema é só um cadastrinho, é muito simples, basta salvar meia dúzia de campos de um formulário."

Por vezes, a afirmação anterior se mostra verdadeira. Para uma primeira iteração, entregar um simples cadastro de seis campos, pode atender às necessidades do momento. Mas é normal que, idéias comecem a borbulhar na cabeça do domain expert, após ver o 'cadastrinho' funcionando.

Aí vem o problema:

Partindo do princípio que desenvolvimento de software é um processo evolutivo, isto é, novas necessidades surgem e mudanças ocorrem em funcionalidades já existentes, a escolha de uma metodologia de desenvolvimento apropriada, que privilegie extensibilidade e manutenibilidade, é essencial para o sucesso do projeto, pois o cadastrinho de 6 campos pode facilmente evoluir para o Cthulhu.

Estudos e a prática, mostram que a maior parte do tempo de um projeto de software, é gasto na fase de manutenção e melhorias. Muitas vezes, por questões de prazos apertados, escolhas inadequadas acabam sendo feitas com relação à implementação, por oferecerem produtividade alta e imediata. Isso pode gerar um design "engessado", pouco extensível e de difícil manutenção, a complexidade pode rapidamente engolir a equipe de desenvolvimento.

Já ouvi muitos colegas dizerem que o sucesso se mede pela satisfação do cliente. Concordo que satisfação é essencial, mas no meu ponto de vista, deixar o cliente satisfeito, é o mínimo que se pode esperar quando se oferece um produto. 

Satisfação do cliente e software com qualidade, não são (ou não deveriam ser) objetivos mutuamente exclusivos. A filosofia de Domain-Driven Design (Eric Evans), introduz ao processo de desenvolvimento de software: 

  • uma arquitetura em camadas, para organização de responsabilidades; 
  • ubiquitous language, para facilitar a comunicação entre equipe técnica e domain experts, além de gerar documentação em nível de código;
  • patterns que simplificam e consolidam o design
Ao utilizá-la, associada ao paradigma orientado a objetos, uma equipe ágil, deve ser capaz de vencer cenários onde os prazos são curtos e os clientes exigentes, com a vantagem de gerar um produto final que reflete o domínio do cliente, com um design extensível, de manutenibilidade e legibilidade simplificadas, tanto para os profissionais envolvidos, quanto para futuros profissionais que venham a se envolver no projeto.


Para saber mais:

agosto 02, 2010

TDD... eu recomendo!

Chega a requisição, de uma nova funcionalidade, para um dos sistemas em que estou envolvido.

O sistema em questão, gerencia a vida do aluno no programa de pós-graduação, stricto sensu, da UERJ. A requisição era algo do tipo:

"Os cursos de pós-graduação, devem poder lançar as notas dos alunos inscritos em suas turmas no semestre."

Meu caminho natural, para começar a atender este pedido, seria: (i) configurar os xmls do STRUTS, (ii) criar actions e (iii) criar telas do sistema. Basicamente, eu trabalharia primeiro as camadas de apresentação, controle e provavelmente de infraestrutura, para abrir caminho até a camada de modelo e a partir daí, iniciar a programação das regras de negócio.

Mas neste dia, decidi testar uma nova abordagem. Eu já tinha lido à respeito, ouvido falar e até aplicado em projetos acadêmicos, a técnica de Test Driven Development ou TDD.

Ao ler a requisição que me foi passada, percebi três funcionalidades principais que precisaria desenvolver: (i) listar para o usuário as turmas do semestre, (ii) listar os alunos da turma selecionada e (iii) permitir o lançamento das notas.

As três funcionalidades descritas acima, tornaram-se meus casos de teste e eu iniciei o desenvolvimento dos testes unitários utilizando as bibliotecas do JUnit.

Por mais que estivesse acostumado a fazer da forma antiga, enxerguei grandes vantagens utilizando o TDD:

  1. Trabalhei diretamente na camada de modelo.
  2. Pude focar meus esforços nos casos de teste, isoladamente, sem me preocupar com as outras funcionalidades a serem desenvolvidas.
  3. Quando parti para as camadas de apresentação e controle, já tinha certeza de que minhas regras de negócio estavam 100% funcionais e sem erros, tive apenas que lidar com erros técnicos (configurações do struts por exemplo) e de camada de apresentação (jsp's).
  4. Os teste unitários, passam a fazer parte do sistema e são artefatos valiosos para a detecção de possíveis bugs futuros.
  5. O desenvolvimento, pelo menos no meu caso, foi mais rápido.

Ao avaliar as vantagens obtidas utilizando o TDD, neste projeto, com certeza aplicarei a técnica em projetos futuros.

julho 31, 2010

UML é sua amiga...

Durante muito tempo, principalmente na época de faculdade, olhava para aquele monte de diagramas sem sentido e me perguntava:

- Como isso pode ajudar alguém ou alguma equipe a desenvolver software?

Sim, eu estava falando de UML.

Acredito que muitos profissionais de TI sintam esta mesma antipatia. Isso talvez se deva ao fato de enxergarem a UML, da mesma forma que eu costumava enxergar, como um PROCESSO.

Antes de mais nada é importante esclarecer o seguinte:

"UML não é um processo, UML é uma ferramenta!"


Isso pode parecer básico, mas faz toda a diferença. Vejamos as figuras a seguir:

Visão geral da UML
Visão geral do SCRUM

A primeira figura, nos dá uma visão geral da UML e seus diagramas, a segunda, fornece uma visão geral do SCRUM.

Se retirarmos qualquer elemento do SCRUM, ele deixa de ser SCRUM, se não utilizarmos certos diagramas da UML em nossos projetos de software, a UML continua sendo UML, continua sendo uma ferramenta de apoio, pois, ao contrário do SCRUM, UML não é um processo.

Utilizar a UML como metodologia de desenvolvimento de software é errado, é contraproducente, torna o processo penoso e se associado à um modelo em cascata, torna as coisas muito piores. No final, você se vê afogado em diagramas que não possuem utilidade prática para o seu domínio e o pior, sem código escrito.

Depois que passei a enxergar UML como uma ferramenta de apoio, passei a usufruir dos seus reais benefícios e ela passou a realmente auxiliar em meus projetos. Basicamente, utilizo 3 diagramas:

  • Diagrama de Classes: Este dispensa comentários, é o mainstream dos diagramas da UML, o popstar. Este diagrama é uma foto do seu modelo. Ele contém os objetos e a forma como eles se relacionam.
  • Diagrama de Sequência: Serve como apoio para descrever funcionalidades do seu sistema. Ele descreve a maneira como objetos colaboram entre si, para atingirem um objetivo.
  • Diagrama de Atividades: Este é um diagrama de uso geral, pode ser utilizado para descrever literalmente, qualquer coisa. É um ótimo diagrama para comunicação entre a equipe, descrição de processos complexos, casos de uso e estórias do cliente.

Com exceção do diagrama de classes, não é sempre que faço uso dos outros diagramas, tudo depende da necessidade da situação.

Esta é apenas uma pequena parte da UML, mas que em geral, atende bem às minhas necessidades. Não se obrigue a utilizar tudo o que é oferecido, utilize a UML como uma ferramenta e com certeza você será beneficiado em seus projetos.

Grandes poderes trazem grandes responsabilidades...

Herança é um recurso poderoso oferecido pela orientação a objetos, mas para aproveitar suas vantagens, é preciso ter cuidado e fazer uso consciente dela.

Herança é um relacionamento fortíssimo entre duas classes. Quando você diz que um coisa herda outra em orientação a objetos, você está realmente dizendo que: "uma coisa além de ser uma coisa, também é outra coisa". Ahn?! O Que?! Como assim?! Parece até diálogo do Oráculo com o Neo em Matrix. Calma, um exemplo simples pode desfazer toda a confusão. Se eu digo que:

class Cachorro extends Animal{}

De fato, eu estou dizendo que "Cachorro além de ser um Cachorro, também é um Animal". Na prática, qualquer variável de referência do tipo Animal, pode apontar para um objeto de Cachorro na Heap.

Dizer que Cachorro é um Animal, faz todo sentido, nossa classe Cachorro além de possuir uma interface própria, carrega toda a interface referente à Animal. Herança tem um preço, ela gera acoplamento entre classes, elas se tornam intimamente ligadas, mas neste contexto, é perfeitamente aceitável, pois é natural que Cachorro seja um Animal. Agora pense, se eu disser:

"Cachorro é uma Cafeteira!"

...

Esquisto não?!...mas eu insisto que Cachorro é uma Cafeteira e codifico:

class Cachorro extends Cafeteira{}

Este código vai compilar e rodar perfeitamente. Nós acabamos de criar uma aberração, um cachorro que faz café. Criamos um acoplamento completamente desnecessário e reduzimos significativamente a coesão da classe cachorro. Uma classe coesa é altamente especializada e possui responsabilidades bem definidas, não é o caso do nosso "Cachorro-Cafeteira".

Vamos dizer que eu não esteja satisfeito com o fato de que meu Cachorro só saiba fazer café, e diga o seguinte:

"Cachorro é uma Cafeteira, um Microondas, um Celular e um Grill George Foreman."

Se o cachorro que faz café já é esquisto, isso aí então...

Java só aceita Herança Simples, isto é, uma classe só pode herdar de outra classe e ponto final. Mas se mesmo com Herança simples, podemos criar um cachorro que faz café, imagine o que pode ser feito utilizando Herança múltipla. C++ é um exemplo de linguagem que utiliza este recurso, onde uma classe pode herdar de várias classes. Herança simples, mal utilizada, pode resultar em classes com responsabilidades mal definidas, isso gera um ônus alto para um projeto orientado a objetos, logo, permitir que classes herdem de diversas classes, pode ser fatal em termos de qualidade de software, pois nada impede que se crie um "Frankenstein", apenas o bom senso.

Java oferece o recurso de interfaces. Uma interface é "uma classe 100% abstrata". Uma classe pode implementar diversas interfaces. Por mais que nada impeça que sua classe implemente interfaces totalmente sem sentido, existe uma vantagem nesta abordagem, pois Interfaces, são livres de implementação, isto é, seus métodos são "ocos", é de responsabilidade da classe implementadora prover funcionalidades aos métodos.

Herança é uma ferramenta muito útil, oferecida pelo paradigma orientado a objetos, porém, utilizá-la com coerência, é responsabilidade do desenvolvedor que preza pela boa qualidade do projeto de software. Lembre-se: Grandes poderes trazem grandes responsabilidades!