Mês: abril 2009

Visão Geral dos Processos da Área de Desenvolvimento de Software

Objetivo

A idéia deste artigo é apresentar um pouco das idéias e impressões que tenho em relação aos processos e metodologias da área de desenvolvimento de software.

O grande desafio do desenvolvimento de software está na diferença de perfis de profissionais necessários para se construir um software de qualidade. São muitas disciplinas e características diferentes para conseguir se construir uma equipe e consequentemente um software de qualidade.

Os processos na área de desenvolvimento existem principalmente para conseguir ter visibilidade sobre o tamanho do projeto (prazo e custo), minimizar os erros na passagem das tarefas entre as diferentes áreas e etapas do projeto.

Costumo dizer que a área de desenvolvimento de software precisa ser extremamente disciplinada pois cada etapa que pulamos no processo custa muito mais caro adiante. É impossível desenvolver uma funcionalidade correta quando o requisito está errado. É praticamente impossível chegar um software com um mínimo de qualidade para o cliente final quando não existe teste. Quando as etapas são puladas é que existe a certeza de ter desgaste entre o cliente, a equipe e os sponsors.

Esse artigo não tem por idéia focar alguma metodologia de desenvolvimento. Apenas discutir um pouco sobre os problemas comuns encontrados na área. Posteriormente talvez arrisque escrever um artigo discutindo um pouco sobre as metodologias de mercado (RUP, XP e outras) e suas características.

Aqui vai a idéia geral das “fases” para se concluir o desenvolvimento de um software, da sua concepção até sua implementação e os desafios encontrados em cada uma das fases.

Falaremos aqui principalmente sobre desenvolvimento de softwares empresariais, mais focados em banco de dados. Cadastros, consultas, relatórios e integrações. O universo do desenvolvimento de software é muito maior e pode compreender softwares embarcados, de missão-crítica, que controlam outros equipamentos. Descartaremos esses exemplos que existem, mas não representam a grande maioria.

Ciclo técnico de Desenvolvimento

Por mais que existam metodologias completamente diferentes, baseadas ou não em UML, com alto feedback, baixo feedback, muito prescritivas, pouco prescritivas todas elas partem de algumas premissas que chamamos de “ciclo técnico” de desenvolvimento.

As etapas-macro do desenvolvimento podem ser vistas dessa forma:

  • Análise: A idéia aqui é saber “o que” fazer.
  • Desenvolvimento: A idéia aqui é pegar o que foi definido na etapa de análise e fazer.
  • Implantação: A idéia aqui é colocar pra rodar.

Olhando dessa forma parece muito fácil. Vamos explodir cada uma dessas etapas e verificar que não é tão simples assim. O mundo real é um pouquinho diferente.

Análise

Visão

O primeiro questionamento que temos na etapa de análise é: como saber quanto o projeto vai custar? Dependendo do custo, fica muito complicado justificar junto aos sponsors a continuidade do projeto.

Mas como vou saber o custo se a etapa de análise pode custar em torno de 40% do projeto?

Aí entram metodologias de análise de ponto de função. Existe muita coisa no mercado sobre esse assunto, porém é muito difícil achar algo que se adeque à realidade de cada uma das empresas que tem algum tipo de desenvolvimento de software. A maioria dessas metodologias é baseada no modelo matemático COCOMO.

É claro que estimar “antes” de fazer todo o levantamento é arriscado. Mas é uma forma de conseguir se ter uma “visão” do tamanho do projeto e qual etapa, funcionalidade é possível trabalhar, adiar, fasear para viabilizar o projeto.

Para chegar numa estimativa e num esforço aproximado, vários fatores são determinantes:

  • Experiência da equipe: Equipes mais juniores tendem a estimar muito mal. Experiência é fundamental para conseguir ter visão.
  • Padrões de Arquitetura: Os padrões ajudam a resolver uma série de problemas enfrentados em desenvolvimentos anteriores. Coisas como “como tratar um cadastro master-detail?” e um cadastro master-detail em “N” níveis? Qual ferramenta usar para gerar relatórios? Equipes sem experiência em desenvolver padrões de arquitetura tendem a fazer “cada um do seu jeito”. Isso dificulta um desenvolvedor dar manutenção no código do outro e chegar em estimativas mais precisas.
  • Histórico. Começar a medir o tempo “real” que as coisas levam ajuda a chegar num tempo mais próximo da realidade em estimativas futuras. Criar a cultura de medir é muito importante.

Para conseguir chegar num número (geralmente hora/homem, ou quanto tempo um desenvolvedor em média levaria para implementar tal requisito, tendo como padrão um desenvolvedor num nível pleno geralmente) deve-se fazer levantamentos preliminares, obtendo as seguintes informações:

  • Quantos cadastros/consultas existem? Entende-se por cadastro/consulta interfaces para dar manutenção em dados simples ou compostos (desde uma tabela de “tipo de pedido”, “parâmetros”, até entidades mais complexas como pedidos (compostos por itens e programações de entrega, por exemplo).
    • Desses cadastros, quantos armazenam dados em uma única tabela? Quantos armazenam em duas, três, quatro tabelas? Esse é um bom fator para determinar complexidade.
    • Quantos campos tem em média em cada uma das tabelas? (Até 5 – simples, até 10 médio, mais de 10, complexo)
    • Quantas tabelas auxiliares precisarão ser consultadas para conseguir realizar esse cadastro?
    • A consulta tende a envolver muitos filtros (até 5, até 10, mais de 10).
  • Existem integrações?
    • Os sistemas integrados são padrão de mercado?
    • Existe viabilidade tecnologica de integração entre o software desenvolvido e o sistema destino?
    • Quantas interfaces existem?
    • Quantos campos em média?
  • Quantos relatórios existem?
    • Quantas tabelas envolvidas para gerar o relatório?
    • Dependem de informações obtidas de algum outro sistema/site/web service? É viável extrair essas informações?
    • Quantos campos em média no relatório?

Através desses fatores, dá para obter através de histórico uma média do esforço para projetos futuros. Em cima disso é possível também declarar escopo para o projeto (é interessante declarar o “número” de cadastros, consultas, interfaces) para evitar desvios futuros. É interessante até que essa declaração de escopo conste na proposta. Fica mais transparente tanto para quem está vendendo, quanto para quem está comprando.

Muitas vezes os projetos são manutenções evolutivas em sistemas já existentes. Nesses casos, deve-se preocupar com alguns fatores adicionais:

  • Caprichar na análise de impacto quando as mudanças envolvem módulos “Core” do sistema a ser modificado. Vale a pena investir tempo aqui, porque geralmente sistemas legados não possuem documentação de projeto, manual e pouquíssima gente sabe como funciona (geralmente o lugar mais confiável para saber o que o sistema faz é o próprio código-fonte).
  • Na ausência de padrões de arquitetura ou manutenções em módulos com código mal escrito (cenário mais comum) considerar na estimativa o tempo perdido com lutas para entender a arquitetura ou desenvolver de um jeito mais custoso. As vezes esse tipo de análise fomenta a migração de um sistema todo para outra tecnologia ou arquitetura.
  • É importantíssimo ter um processo de gestão de configuração e versões para garantir a mudança de uma forma saudável. Manutenções evolutivas em softwares legados sem esse tipo de processo tendem a ser desastrosas, pois não consegue-se rastrear o que mudou e voltar as alterações como contingência de algum problema mais sério, principalmente em sistemas que envolvam casos de multa, penalização ou processos críticos (billing, por exemplo).

Levantamento de Requisitos

Aqui faremos uma abordagem bem superficial. A idéia é detalhar algumas técnicas de levantamento num artigo separado.

O grande desafio da etapa de levantamento de requisitos está em extrair as informações dos usuários. É um desafio por uma série de razões.

A principal delas na minha opinião é que raramente os usuários sabem o que querem. Eles querem um sistema que resolva o problema deles, mas geralmente nunca pararam pra pensar no que a ferramenta deve fazer para ajudá-lo. Por isso as solicitações podem ter desvios absurdos em relação ao escopo declarado na etapa anterior. As vezes começa-se um sistema de faturamento por exemplo e percebe-se que no meio do caminho a dificuldade de se emitir as notas fiscais e faturas a tempo está nas informações de um sistema de estoque por exemplo, mas o escopo do projeto é desenvolver um sistema de faturamento.

Outro fator que geralmente prejudica o levantamento são usuários que “seguram” informações. Tem medo de serem obsoletados pelo novo sistema e acabam boicotando o projeto. Para esse tipo de usuário é interessante ter uma conversa mais próxima e mostrar futuras oportunidades que ele pode ter caso o projeto tenha sucesso. É muito importante estar atento a esses detalhes. Ambiente de guerra e conflito não é um bom ambiente para executar trabalhos criativos.

O perfil da pessoa responsável pela análise e documentação dos requisitos é um perfil bem “raro” no mercado. Na minha opinião o ideal são pessoas que evoluiram da programação e passaram a compreender um pouco mais de negócio e tem boas características de relacionamento pessoal. São comunicativos, bons argumentadores e extremamente pacientes com os usuários. Há quem acredite que uma pessoa que realiza análise de sistemas não precisa saber desenvolver. Há controvérsias. Eu acredito que ele não precise ser um exímio desenvolvedor ou mesmo gostar de código-fonte, mas precisa ter visão de processo e principalmente do modelo entidade-relacionamento (ou o diagrama de classes para quem usa).

Para “documentar” os requisitos existem várias formas. Pretendo detalhar algumas delas quando me aprofundar no assunto “Levantamento de Requisitos”.

O importante é concluir essa fase com um documento descrevendo a funcionalidade do ponto de vista do usuário. O que tem, como se comporta, de onde vem a informação (outro cadastro, outro sistema). É importante ter uma aprovação “formal” do usuário final. Para que no caso de mudanças futuras, tenha-se um ponto “base” para discutir.

No caso de manutenções de sistemas, nada impede que o comportamento do sistema atual seja descrito e o que será modificado no mesmo documento. O importante é que com este documento seja possível ter um ponto base para discutir com o usuário no caso de futuras modificações.

Desenvolvimento

A etapa de desenvolvimento consiste em “fazer” o software. Como pré-requisito, as especificações funcionais da etapa de análise devem estar prontas e consistentes.

Geralmente, dependendo dos profissionais que realizam a análise, existe grande chance delas não estarem 100% consistentes e num nível de detalhamento bom (com todas as informações referentes aos comportamentos do software esperado informadas). É interessante colocar um processo “formal” de aceite, onde os desenvolvedores validem essas especificações e “aceitem” as mesmas da equipe de análise antes de começar a contar o tempo de desenvolvimento.

Algumas equipes de desenvolvimento desprezam especificações técnicas. As especificações técnicas basicamente são descrições de “como implementar” o software, em termos bem técnicos. O que usar, detalhamento de lógicas de programação e outros. Isso está muito relacionado com o perfil das pessoas que estão desenvolvendo. Quando existem muitos programadores inexperientes, é interessante que estes recebam as coisas muito bem especificadas. Quando se trabalha com perfis de analista/programador é possível não usar especificações técnicas e ter sucesso.

Existem algumas metodologias que utilizam ferramentas da UML como diagrama de classes, seqüência, componentes para descrever a implementação. Particularmente acho que estes diagramas podem ser “opcionais” na maioria dos casos (os mais simples). Vai muito do critério de quem é o líder técnico da equipe e a maturidade desta equipe. Vale lembrar que os diagramas de classes ou descrições da especificação técnica são um processo bastante custoso e raramente evoluem junto com o software. Eu acredito que vale mais não ter um documento no projeto do que ter um documento desatualizado. Documentos desatualizados atrapalham invés de ajudar.

Padrões de arquitetura são fundamentais. Geralmente os times tem os padrões deles já descritos **antes** de começar o projeto. A idéia aqui está em já ter formatadas e prontas soluções para os problemas mais comuns: Como paginar grids na Web, padrão de navegação, como tratar cadastros master-detail (em vários níveis), padrão para desenvolvimento de relatórios, processamentos batch e outros. Ter esses padrões desenvolvidos e maduros agiliza muito o desenvolvimento e melhora muito a qualidade do produto final.

Os testes “unitários” são de responsabilidade do desenvolvedor. Faz parte da etapa de desenvolvimento. É muito interessante que o líder técnico monitore por “amostragem” o código escrito pelos demais desenvolvedores, para saber se os padrões estão sendo seguidos e se os mesmos estão testando o código antes de liberar para testes. Ferramentas de controle de versão que “notificam” alterações ajudam muito na gestão desse tipo de alteração.

Homologação Interna

Testes

Teste é um assunto sempre complicado. Geralmente as pessoas que testam são usuários de sistema experientes e não conhecem a fundo o sistema. Não é raro pegar pessoas que sequer conhecem o ciclo de desenvolvimento de software e mesmo não tem muita experiência com informática. Ex.: Um testador de um sistema Web que não entende como funciona o “cache” de um browser.

O mais importante aqui é garantir que as funcionalidades estejam desenvolvidas de acordo com a especificação funcional. Todas as funcionalidades previstas na especificação devem estar de acordo com o especificado. Especificações que contém “casos de teste” ajudam bastante. Casos de teste são exemplos de como operar o sistema, e como o sistema deve se comportar nessas situações.

Testes de operação simples também devem ser contemplados. Estouro de tamanho de campos, informar letras em campos numéricos e coisas não-óbvias que geralmente os usuários fazem e os desenvolvedores acabam não prevendo.

Para a área de testes é importante criar “scripts” documentados sobre quais processos devem ser feitos no sistema para dar “aceite” na funcionalidade. À medida que surgem novas situações, alimentar esses scripts. Eles ajudaram em futuras manutenções para evitar que situações já previstas ocorram após uma manutenção.

Aqui devem ser previstos os testes “integrados”, ou seja, o impacto de uma funcionalidade nas demais (ligou um parâmetro aqui, o que muda ali?) e testes de integração entre sistemas.

Homologação Interna

Uma vez testado, o sistema encontra-se num estágio em que está funcionando e é muito importante que os analistas que participaram do levantamento façam a “passagem” para a equipe de implantação. Esse processo costumo chamar de “homologação”, ou seja, verificar se o que foi desenvolvido está de acordo com a especificação funcional, pelas pessoas que conceberam a espeficação funcional e conhecem a real necessidade do cliente.

Se isso for realizado antes dos testes, é possível que a quantidade de bugs existentes ainda no sistema e erros básicos acabem transformando a equipe de análise em testadores, o que é um custo completamente desnecessário.

Uma vez os analistas considerando que o sistema está 100% de acordo com o desenvolvido, passa-se para a etapa de homologação junto aos usuários.

Não é raro nessa etapa surgirem discussões sobre coisas o sistema “deveria” contemplar. O juiz para esse tipo de situação é sempre a especificação funcional. Veremos mais adiante o tópico “Aumentos de Escopo” que fala um pouco sobre esses “desvios”.

Homologação junto aos usuários

Nesse processo a equipe de implantadores já deve ter conhecimento do funcionamento do sistema e junto aos analistas (fica a critério da empresa, qual ou quais equipes mandar. Mandar as duas é uma opção interessante para disseminar o conhecimento e evitar que o cliente ou usuário final aproveite-se da insegurança de quem está conduzindo a homologação para ganhar funcionalidades novas) devem se reunir com o usuário final e validar as solicitações junto a ele.

Nesse processo é muito comum senão inevitável surgirem questionamentos relacionados ao sistema. Falta isso, falta aquilo, isso é bug, isso não deveria funcionar dessa maneira. Aqui é que a especificação funcional paga todo o seu investimento.

É muito simples, mas exige bastante firmeza e disciplina:

  • Se o erro for grosseiro do tipo, mensagem de erro em inglês não tratada, campo numérico aceitando valor string ou coisas parecidas, não tem nem o que questionar. É bug, gera um issue (importante ter um sistema de issue tracking).
  • Se a solicitação do usuário estiver contemplada na especificação funcional e não estiver contemplada no software, é bug. Gera um issue.
  • Se a solicitação do usuário não estiver contemplada na especificação (que o próprio usuário aprovou), é uma situação de aumento de escopo do projeto. Deve-se negociar o custo para fazer a alteração.

Aumentos de Escopo

No caso de aumento de escopo é muito comum a equipe de gestão de projeto ou mesmo a equipe comercial, não ir atrás de justificar essas horas com o cliente. É sempre 1h aqui, 2 ali, um dia aqui. E ninguém faz a soma de quanto perdeu no final. Não é raro jogar uma quantidade de mais de 50% de aumento de escopo num projeto, pois os usuários raramente sabem o que querem e não entendem o custo envolvido no desenvolvimento de um software. Como quem geralmente paga a conta é o fornecedor ou a área que desenvolve o software, os usuários acabam não se incomodando em pedirem coisas novas indiscriminadamente.

Sugiro que a equipe de desenvolvimento “feche as portas” para solicitações desse tipo não-especificadas. Quem tem que entender a solicitação do cliente, analisar o impacto e especificar a alteração é a equipe de análise. Quando vierem as especificações revisadas, devidamente aprovadas pelo cliente, deve-se estimar as horas e contabilizar no projeto o quanto houve de desvio em relação às horas estimadas no inicio. É a única forma de criar uma cultura na equipe de sempre estimar horas, dar visibilidade de onde ocorrem os problemas e ainda e melhorar a receita dos projetos. Os problemas geralmente se classificam em algum destes:

* Usuário chave que não sabe exatamente o que quer
* Equipe de análise com dificuldade para entender o que o usuário precisa
* Equipe de desenvolvimento sendo displicente e propositalmente “esquecendo” de implementar requisitos para não perder seus prazos.

Implantação

Plano de Implantação

Deve ser traçado junto aos usuários um plano, com datas e responsabilidades para realizar a implantação do projeto. Algumas etapas críticas:

  • Treinamento dos usuários
  • Levantamento de cargas/dados iniciais para o sistema começar a funcionar
  • Coordenação de subida da aplicação para o ambiente de produção
  • Alterações/processos necessários em sistemas integrados

Uma vez traçado esse plano, é segui-lo e acompanhar o ambiente de produção. É interessante deixar alguns desenvolvedores, analistas em stand-by para atender os clientes em possíveis problemas (já que é um processo novo e geralmente gera-se uma série de dúvidas).

Solicitações Extra-Escopo

Agora não partindo do usuário chave e sim do usuário final, é comum surgirem mais questionamentos sobre como o sistema deve funcionar.

Por isso é importante conseguir manter um ótimo relacionamento com o usuario-chave e fazer ele se que sua colaboração é imprescindível para o sistema (já que o usuário chave é mesmo uma das figuras mais importantes do sistema!). Ele é o primeiro defensor do sistema.

No caso da impossibilidade de evitar que novas solicitações sejam colocadas para que o projeto entre em produção, deve-se seguir o mesmo processo do item “Aumentos de Escopo”.

Conclusão

Como vimos na maioria dos tópicos, disciplina é imprescindível. Se as etapas são puladas, os problemas não aparecem. Cria-se um ambiente de projeto em que as pessoas ficam mais preocupadas em se defender do que em colaborar.

O sucesso do sistema está no trabalho cooperativo entre equipes multi-disciplinares. É praticamente impossível conseguir sucesso quando se estabelece um clima de guerra na equipe.

Os processos parecem tornar o desenvolvimento mais “chato”. Eu discordo totalmente disso. Quando as pessoas começam a adquirir facilidade e disciplina para realizar todas essas etapas, cada um começa a ter clara a sua importância e responsabilidade no processo, diminuem-se as falhas e dessa forma o trabalho fica mais produtivo e agradável.

Spring.NET – Parte 3 – Suporte ADO e Transação

Objetivo

Nesta parte do tutorial, falaremos um pouco do suporte do Spring.Net ao ADO.Net e Transações. A idéia central dessa parte do tutorial está em explicar a idéia de configurar as transações na aplicação como um “aspecto” e não como código “pregado” na aplicação.

Como pré-requisito dessa parte do tutorial, somente a parte 1 (Dependency Injection) é suficiente. Porém, nada impede da idéia de transação ser usada em conjunto com o suporte a Web Services do Spring. Resolvi fazer em cima da parte 1, justamente para explicar separadamente cada um dos conceitos.

Conceito de Transação no Spring.Net

Aqui não tem nenhuma novidade a idéia de uma transação é o velho conceito de “ou tudo acontece ou nada acontece”. O usuário vai gravar um pedido com todas as suas informações. Um pedido, N itens, cada um dos itens com N programações de entrega. Ou o pedido acontece inteiro na base de dados, ou não acontece. Se acontecer “somente um pedaço”, teremos um problema (pedido sem itens, itens sem programação de entrega).

Esse conceito começou nos bancos de dados relacionais, onde se tem a idéia de iniciar uma transação, fazer uma série de atualizações. Se tudo deu certo, damos um “commit” no final e tudo acontece. Se algo der errado, damos um “rollback” no final e nada acontece.

Na teoria isso é muito bonito e simples, mas na prática é comum vermos o nosso código com o controle de transação misturado no meio da regra de negócio. Por exemplo, vamos imaginar o seguinte código na nossa camada de negócio (reusável por mais de uma UI):

public void gravarPedido(Pedido p){
  //Consiste dados do cabeçalho do pedido
  //outras consistências de item.

  //Gravação transacional do pedido.
  using (TransactionScope tx = new TransactionScope){
    //Cria conexão, grava cabeçalho do pedido
    foreach (ItemPedido ip in p.Itens){
      //Grava item do pedido
    }
  }
}

Peraí… o código é para ter regra de negócio ou controle de transação? Será que não estamos “sujando” nossa regra de negócio?

Dessa forma, podemos criar um método que encapsula as regra de negócio do pedido e da transação, porém, se formos utilizá-lo em outro método transacional que “gera pedidos”, passamos a ter problema, pois quem manda na transação?

Então teríamos outra saída, delegar o gerenciamento da transação para a UI. Aí caímos na questão… faz sentido a interface com o usuário ser responsável pela transação? Se o programador que está consumindo uma regra de negócio pré-escrita “esquecer” da transação, tudo vai por água abaixo.

A idéia do Spring aqui é justamente resolver o problema da transação pensando na mesma como um aspecto (ou requisito não-funcional).

Antes de explicarmos como funciona esse conceito no Spring, devemos entender que para que possamos usufruir de suas facilidades é necessário usar uma framework de acesso a dados que suporte o Spring. No caso do ADO.net, o Spring fez algumas evoluções para que ele funcione junto com o seu conceito de transação. O Spring também suporta NHibernate, mas não é o foco desse artigo. É possível também implementar suporte do Spring para outras frameworks de mapeamento objeto/relacional ou acesso a dados (o Spring oferece mecanismos para isso).

Suporte do Spring ao ADO.Net

Seguindo a idéia do nosso exemplo, o tutorial de Spring parte 1 (Spring.Net – Parte 1 – Dependency Injection), vamos implementar uma nova camada de persistência, usando Spring.Data.

O suporte ADO do Spring.Net tem por objetivo “desacoplar” também qual “provider” do ADO está sendo utilizado, por isso utiliza sempre interfaces como IDbConnection, IDbDataReader e amigos. Dessa forma, se trocarmos o provider e escrevermos queries em “ANSI-SQL”, podemos facilmente ter uma aplicação que suporte vários bancos de dados sem grandes dores de cabeça.

Modelo E/R

Fugi o tempo inteiro de usar base de dados nesses exemplos, mas infelizmente agora não vai dar mais para escapar. Criei um exemplo usando o SQL Server. Daria pra fazer usando outro provider sem grandes problemas.

As tabelas que precisamos são as seguintes:


create table Pedido(
  PedidoID int identity(1,1) not null,
  ClienteID int not null,
  ValorTotal float not null
  constraint PK_Pedido primary key (PedidoID)
)

create table ItemPedido(
  ItemPedidoID int identity(1,1) not null,
  PedidoID int not null,
  ProdutoID int not null,
  Quantidade float not null,
  PrecoUnitario float not null
  constraint FK_Pedido foreign key (PedidoID) references Pedido (PedidoID)
  constraint PK_ItemPedido primary key (ItemPedidoID)
)

Implementando a camada de Persistência no nosso Exemplo

Primeiro vamos criar na nossa solution um novo assembly e batizá-lo de DAL.ADO.SQLServer. Esse assembly deve referenciar os seguintes assemblies:

  • Model (porque ele precisa conhecer os VO’s para persistir)
  • DAL.Interface (porque ele precisa implementar a interface da nossa camada de acesso a dados para poder ser “injetado” pelo Spring).

Vamos criar nesse assembly as classes PedidoDAO e ItemPedidoDAO:

PedidoDAO.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DAL.Interface;
using System.Data.SqlClient;
using Model;
using System.Data;
using System.Data.SqlTypes;
using Spring.Data.Common;
using Spring.Data.Generic;
using Spring.Data;

namespace DAL.ADO.SQLServer {
  public class PedidoDAO : AdoDaoSupport, IPedidoDAO {    
  
    #region IPedidoDAO Members

    public void inserirPedido(Pedido p) {      
      IDbParametersBuilder builder = CreateDbParametersBuilder();
      builder.Create().Name("ClienteID").Type(DbType.Int32).Value(p.ClienteID);
      builder.Create().Name("ValorTotal").Type(DbType.Double).Value(p.ValorTotal);
    
      string sql = 
        "insert into Pedido (ClienteID, ValorTotal) " +
        "values (@ClienteID, @ValorTotal) \n" +
        "select SCOPE_IDENTITY()";
                                
      decimal d = (decimal)AdoTemplate.ExecuteScalar(CommandType.Text, sql, builder.GetParameters()); 
      p.PedidoID = Convert.ToInt32(d);      
    }

    public void alterarPedido(Pedido p) {    
      string sql = 
        "update Pedido " +
        "set " +
        "  ValorTotal = @ValorTotal, " +
        "  ClienteID = @ClienteID " +
        "where " +
        "  PedidoID = @PedidoID";
        
      IDbParametersBuilder builder = CreateDbParametersBuilder();
      builder.Create().Name("@ValorTotal").Type(DbType.Double).Value(p.ValorTotal);
      builder.Create().Name("@ClienteID").Type(DbType.Int32).Value(p.ClienteID);
      builder.Create().Name("@PedidoID").Type(DbType.Int32).Value(p.PedidoID);
      
      AdoTemplate.ExecuteNonQuery(CommandType.Text, sql);      
    }

    public void excluirPedido(Pedido p) {
      string sql = 
        "delete from Pedido " +
        "where PedidoID = @PedidoID";

      IDbParametersBuilder builder = CreateDbParametersBuilder();
      builder.Create().Name("PedidoID").Type(DbType.Int32).Value(p.PedidoID);
      AdoTemplate.ExecuteNonQuery(CommandType.Text, sql);
    }
    
    private void mapearLinhaPedido(IDataReader dataReader){
      
    }

    public Model.Pedido pegarPorID(int PedidoID) {
      string sql = 
        "select PedidoID, ClienteID, ValorTotal from Pedido where PedidoID = @PedidoID";

      IDbParametersBuilder builder = CreateDbParametersBuilder();
      builder.Create().Name("PedidoID").Type(DbType.Int32).Value(PedidoID);           
            
      IList<Pedido> l = AdoTemplate.QueryWithRowMapper<Pedido>(CommandType.Text, sql, new PedidoRowExtractor(), builder.GetParameters());
      if (l.Count > 0)
        return l[0];
      else
        return null;      
    }

    #endregion
  }
  
  public class PedidoRowExtractor: IRowMapper<Pedido>{

    #region IRowMapper<Pedido> Members

    Pedido IRowMapper<Pedido>.MapRow(IDataReader reader, int rowNum) {
      Pedido p = new Pedido();
      p.PedidoID = reader.GetInt32(0);
      p.ClienteID = reader.GetInt32(1);
      p.ValorTotal = (float)reader.GetDouble(2);
      return p;
    }

    #endregion

  }
}

ItemPedidoDAO.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DAL.Interface;
using System.Data.SqlClient;
using System.Data;
using Model;
using Spring.Data.Generic;
using Spring.Data.Common;
using Spring.Data;

namespace DAL.ADO.SQLServer {
  public class ItemPedidoDAO : AdoDaoSupport, IItemPedidoDAO {
  
    #region IItemPedidoDAO Members

    public void InserirItemPedido(Model.ItemPedido ip) {
      string sql = 
        "insert into ItemPedido (PedidoID, ProdutoID, Quantidade, PrecoUnitario) " +
        "values (@PedidoID, @ProdutoID, @Quantidade, @PrecoUnitario) \n " + 
        "select SCOPE_IDENTITY()";
        
      IDbParametersBuilder builder = CreateDbParametersBuilder();
      builder.Create().Name("PedidoID").Type(DbType.Int32).Value(ip.PedidoID);
      builder.Create().Name("ProdutoID").Type(DbType.Int32).Value(ip.ProdutoID);
      builder.Create().Name("Quantidade").Type(DbType.Double).Value(ip.Quantidade);
      builder.Create().Name("PrecoUnitario").Type(DbType.Double).Value(ip.PrecoUnitario);
      
      decimal d = (decimal)AdoTemplate.ExecuteScalar(CommandType.Text, sql, builder.GetParameters());
      ip.ItemPedidoID = Convert.ToInt32(d);
    }

    public void AlterarItemPedido(Model.ItemPedido ip) {
      string sql =
        "update ItemPedido " +
        "set " +
        "  PedidoID = @PedidoID, " +
        "  ProdutoID = @Produto, " +
        "  Quantidade = @Quantidade, " +
        "  PrecoUnitario = @PrecoUnitario " +
        "where " + 
        "  ItemPedidoID = @ItemPedidoID ";

      IDbParametersBuilder builder = CreateDbParametersBuilder();
      builder.Create().Name("PedidoID").Type(DbType.Int32).Value(ip.PedidoID);
      builder.Create().Name("ProdutoID").Type(DbType.Int32).Value(ip.ProdutoID);
      builder.Create().Name("Quantidade").Type(DbType.Double).Value(ip.Quantidade);
      builder.Create().Name("PrecoUnitario").Type(DbType.Double).Value(ip.PrecoUnitario);            
      builder.Create().Name("ItemPedidoID").Type(DbType.Int32).Value(ip.ItemPedidoID);

      AdoTemplate.ExecuteNonQuery(CommandType.Text, sql, builder.GetParameters());
    }

    public void ExcluirItemPedido(Model.ItemPedido ip) {
      string sql =
        "delete from ItemPedido " + 
        "where ItemPedidoID = @ItemPedidoID ";

      IDbParametersBuilder builder = CreateDbParametersBuilder();
      builder.Create().Name("ItemPedidoID").Type(DbType.Int32).Value(ip.ItemPedidoID);        

      AdoTemplate.ExecuteNonQuery(CommandType.Text, sql, builder.GetParameters());
    }

    public List<Model.ItemPedido> LerPorPedidoID(int PedidoID) {
      List<ItemPedido> l = new List<ItemPedido>();
      string sql =
        "select ItemPedidoID, PedidoID, ProdutoID, Quantidade, PrecoUnitario " +
        "from ItemPedido where PedidoID = @PedidoID";

      IDbParametersBuilder builder = CreateDbParametersBuilder();
      builder.Create().Name("PedidoID").Type(DbType.Int32).Value(PedidoID);                      
      
      IList<ItemPedido> l2 = AdoTemplate.QueryWithRowMapper<ItemPedido>(CommandType.Text, sql, new ItemPedidoRowMapper(), builder.GetParameters());
      foreach(ItemPedido ip in l2)
        l.Add(ip);
        
      return l;
    }
    
    
    public class ItemPedidoRowMapper: IRowMapper<ItemPedido>{      
          
      #region IRowMapper<ItemPedido> Members

      public ItemPedido MapRow(IDataReader reader, int rowNum) {
        ItemPedido ip = new ItemPedido();
        ip.ItemPedidoID = reader.GetInt32(0);
        ip.PedidoID = reader.GetInt32(1);
        ip.ProdutoID = reader.GetInt32(2);
        ip.Quantidade = (float)reader.GetDouble(3);
        ip.PrecoUnitario = (float)reader.GetDouble(4);
        return ip;
      }

      #endregion
      
    }

    #endregion
  }
}

Entendendo um pouco do Spring.Data

Vamos discutir um pouco a implementação dessa persistência:

  public class PedidoDAO : AdoDaoSupport, IPedidoDAO {    
    //...
  }

As classes de persistência na camada de acesso a dados devem herdar de AdoDaoSupport. Essa classe possui internamente a propriedade “AdoTemplate”. O AdoTemplate é uma classe do Spring com os métodos para suporte ao ADO.Net e é o principal ponto.

A implementação do AdoTemplate será “injetada” na configuração do contexto do Spring, por isso, não devemos nos preocupar com “de onde ela vem”. Não agora.

    public void inserirPedido(Pedido p) {      
      IDbParametersBuilder builder = CreateDbParametersBuilder();
      builder.Create().Name("ClienteID").Type(DbType.Int32).Value(p.ClienteID);
      builder.Create().Name("ValorTotal").Type(DbType.Double).Value(p.ValorTotal);
    
      string sql = 
        "insert into Pedido (ClienteID, ValorTotal) " +
        "values (@ClienteID, @ValorTotal) \n" +
        "select SCOPE_IDENTITY()";
                                
      decimal d = (decimal)AdoTemplate.ExecuteScalar(CommandType.Text, sql, builder.GetParameters()); 
      p.PedidoID = Convert.ToInt32(d);      
    }

“CreateDbParametersBuilder” é um método que vem da classe AdoDaoSupport. É um “facilitador” para criar parâmetros, independente do banco de dados que está instanciado. Sempre são usados tipos de “DbType”, e na hora de se chamar builder.GetParameters(), o Spring automaticamente instancia os parâmetros do tipo que o provider do ADO precisa. O prefixo do parâmetro (@) não é necessário também. O AdoTemplate automaticamente o adiciona de acordo com a necessidade do banco de dados, justamente para minimizar a dependência entre os bancos.

O exemplo acima, utiliza AdoTemplate.ExecuteScalar, para poder devolver no PedidoID o identity gerado pelo banco de dados. Por que o identity retorna como um decimal é um mistério pra mim e também não é objetivo do meu artigo tentar desvendá-lo.

    public Model.Pedido pegarPorID(int PedidoID) {
      string sql = 
        "select PedidoID, ClienteID, ValorTotal from Pedido where PedidoID = @PedidoID";

      IDbParametersBuilder builder = CreateDbParametersBuilder();
      builder.Create().Name("PedidoID").Type(DbType.Int32).Value(PedidoID);           
            
      IList<Pedido> l = AdoTemplate.QueryWithRowMapper<Pedido>(CommandType.Text, sql, new PedidoRowExtractor(), builder.GetParameters());
      if (l.Count > 0)
        return l[0];
      else
        return null;      
    }

Esse método já é um pouco mais complexo, pois ele “mapeia” o retorno da query para o VO. A parte de escrever a query e passar os parâmetros é idêntico ao método anterior, porém, a hora de pegar o retorno usamos o método “QueryWithRowMapper”.

Esse método, recebe como parâmetro um IRowMapper, implementado na classe abaixo:

    Pedido IRowMapper<Pedido>.MapRow(IDataReader reader, int rowNum) {
      Pedido p = new Pedido();
      p.PedidoID = reader.GetInt32(0);
      p.ClienteID = reader.GetInt32(1);
      p.ValorTotal = (float)reader.GetDouble(2);
      return p;
    }

Pra resumir, ao executar o método, o AdoTemplate cria internamente o SqlCommand (ou o command apropriado ao DbProvider em questão, é transparente), executa a query, pega o datareader de retorno e faz um loop nele. Para cada registro encontrado, faz uma chamada em “MapRow”, para obter o retorno, e já retorna a lista prontinha.

Existem uma série de outros métodos auxiliares como QueryWithResultSetExtractor, QueryWithRowCallback e QueryWithCommandCreator, para facilitar o mapeamento dos resultados para VO’s.

Configurando o Contexto

Agora que nossa implementação está completa, vamos à configuração. Primeiro vamos criar uma referência para o DAL.ADO.SqlServer na nossa Web Application, para que o Spring encontre o assembly no “bin” da WebApplication quando for instanciar nossa camada de persistência.

É necessário adicionar também uma referência para

Em seguida, vamos fazer as configurações no Web.config. Vamos adicionar o seguinte nó dentro de /

<section name="parsers" type="Spring.Context.Support.NamespaceParsersSectionHandler, Spring.Core"/>

Essa configuração, permite que parsers sejam especificados dentro do nó a seção . Então vamos adicionar dentro do nó :

    <parsers>      
      <parser type="Spring.Data.Config.DatabaseNamespaceParser, Spring.Data" />
    </parsers>

Essa configuração, permite que o namespace “http://www.springframework.net/database&#8221; possa ser usado na configuração. Vamos adicionar então no nó o atributo xmlns:db:

<objects 
      xmlns="http://www.springframework.net"
      xmlns:db="http://www.springframework.net/database"     
      >
  <!-- aqui vão outros nós -->
</objects>

Por último vamos adicionar dentro do nó / os seguintes nós:

      <object id="PedidoDAO" type="DAL.ADO.SqlServer.PedidoDAO, DAL.ADO.SqlServer">
        <property name="AdoTemplate" ref="adoTemplate" />
      </object>
      <object id="ItemPedidoDAO" type="DAL.ADO.SqlServer.ItemPedidoDAO, DAL.ADO.SqlServer" >
        <property name="AdoTemplate" ref="adoTemplate" />
      </object>
      <object id="PedidoBO" type="BLL.Implementation.PedidoBO, BLL.Implementation" autowire="byType" />
      <object type="~/Default.aspx" >
        <property name="pedidoBO" ref="PedidoBO" />
      </object>
      <db:provider id="DbProvider"
            provider="SqlServer-2.0"
            connectionString="server=(local);user=sa;pwd=123456;initial catalog=SpringTeste"/>
      <object id="adoTemplate" type="Spring.Data.Generic.AdoTemplate, Spring.Data">
        <property name="DbProvider" ref="DbProvider" />
      </object>

Essas configurações especificam o seguinte:

  • PedidoDAO: Objeto de persistência para o pedido, usando a implementação da nossa camada de acesso a dados recém-criada. Aqui estamos setando manualmente o AdoTemplate para a instância que será criada posteriormente.
  • ItemPedidoDAO: Idem, para item de pedido
  • PedidoBO: nada muda em relação à parte 1 do tutorial.
  • ~/Default.aspx: nada muda em relação à parte 1 do tutorial.
  • DbProvider: O registro daquele namespace “database” é o q permite que o prefixo “db” seja reconhecido pela web.config. O DBProvider é um objeto do Spring que encapsula o Provider do ADO para o SQLServer. Existem vários outros providers para vários outros bancos. Na documentação do Spring existe a lista completa http://www.springframework.net/doc-latest/reference/html/ado.html.
  • adoTemplate: Instância do AdoTemplate, apontando para o provider instanciado, que será “injetada” na nossa camada de persistência.

Como vemos, toda a parte de instância e configuração da conexão com o banco de dados é feita via configuração.

Se executarmos nossa aplicação agora, veremos que ela funciona exatamente como na parte 1 do tutorial, porém, persistindo e recuperando todas as informações de um banco de dados SQL Server.

Transação

Calma. Eu não iria fazer o leitor ter todo esse trabalho se não tivesse algum ganho significativo.

Agora vamos começar a configurar a parte interessante da coisa que é o controle de transação. Para configurar a transação, vamos adicionar o seguinte nó na Web.config, dentro do nó spring/parsers:

<parser type="Spring.Transaction.Config.TxNamespaceParser, Spring.Data"/>

Essa configuração permite que o Spring passe a reconhecer o namespace http://www.springframework.net/tx. Para podermos utilizá-lo, vamos adicionar o namespace no nó objects, ficando ele da seguinte forma (não precisa alterar os nós dentro do nó objects):

<objects 
      xmlns="http://www.springframework.net"
      xmlns:tx="http://www.springframework.net/tx"
      xmlns:db="http://www.springframework.net/database"     
      >

Agora, vamos adicionar dentro do nó spring/objects os seguintes nós:

<object id="transactionManager"	type="Spring.Data.Core.AdoPlatformTransactionManager, Spring.Data">
        <property name="DbProvider" ref="DbProvider"/>
      </object>
      <tx:attribute-driven transaction-manager="transactionManager" />

Esses objetos no contexto servem pra duas coisas: O transactionManager instancia a classe do spring que gerencia as transações. Nesse caso, para gerenciar, estamos usando a “AdoPlatformTransactionManager”, que basicamente encapsula uma transação do próprio ADO (outras opções seriam uma transação do tipo System.Transactions ou transações distribuídas). A tag tx:attribute-driven na verdade é uma forma resumida de configurar os recursos de AOP do Spring (que serão estudados num próximo artigo) a mapear todos os métodos anotados com [Transaction] e torná-los transacional. Existem outras formas de fazer isso. Ex.: Configurar todos os métodos que começam com “do” para serem transacionais, ou simplesmente configurar todo e qualquer método exposto num service Spring para serem transacionais.

Como no nosso exemplo usamos a “transação orientada a atributos”, precisamos “anotar” nosso método como Transacional. Vamos então na classe PedidoBO, colocar a seguinte anotação (É necessária uma referência para Spring.Data, para encontrar a definição de TransactionAttribute):

    [Transaction]
    public void inserirPedido(Model.Pedido p) {
      //...
    }

Pronto. Adicionando essa configuração, tudo que acontece dentro de “inserirPedido” automaticamente é transacional. Se esse método por sua vez utilizar outro que seja transacional, automaticamente o método de “fora” será o transacional, de forma que para a aplicação fica transparente a configuração da transação (se é distribuída, Ado, etc.) e o código de negócio não fica “poluído” com código relacionado à acesso ao banco de dados.

Para confirmar que de fato a transação está ocorrendo, vamos alterar o código do nosso “inserirPedido” no PedidoBO para o seguinte:

    [Transaction]
    public void inserirPedido(Model.Pedido p) {
      //Calcula automaticamente o valor total do cabeçalho do pedido baseado nos itens. 
      //Só pra exemplificar uma regra de negócio.
      float valorTotal = 0;
      foreach(ItemPedido ip in p.Itens){
        valorTotal += (ip.Quantidade * ip.PrecoUnitario);
      }
      p.ValorTotal = valorTotal;
      
      //Persiste o pedido e seus itens.
      _pedidoDAO.inserirPedido(p);
      //Se colocarmos um breakpoint na linha abaixo veremos que p.PedidoID já recebeu até o identity do banco.
      foreach(ItemPedido ip in p.Itens){
        ip.PedidoID = p.PedidoID; //Passa o ID com autoincremento para os filhos.
        _itemPedidoDAO.InserirItemPedido(ip);
      }
      throw new Exception("Vamos testar a transação"); //Quando chegamos aqui, automaticamente ocorre o rollback da transação
    }

Agora vamos colocar um breakpoint em cima da linha com “foreach” para verificarmos que de fato a transação acontece, pois quando uma exception é gerada dentro do método, automaticamente ocorre o rollback do pedido e itens inseridos.

Conclusão

Vemos que além dos ganhos significativos que temos no isolamento do “provider” ADO através dos recursos do Spring.Net, temos nas transações declarativas uma poderosa ferramenta para “desacoplar” o código de controle de transação do código de negócio. Esse processo facilita tanto o reuso da regra de negócio quanto minimiza a dependência de código específico de banco de dados na aplicação (Ex.: SqlDbType nos parâmetros).

Nesse artigo vemos um “exemplo” de como usar a transação. Utilizando um Spring.Data.Core.TxScopeTransactionManager como transaction manager por um exemplo, automaticamente nossa aplicação passará a usar transações do tipo “System.Transactions”. Quando uma transação passa a operar entre duas bases de dados diferentes, o TxScope automaticamente “promove” a transação local para uma transação distribuída. Em outras palavras, automaticamente conseguimos fazer que o Spring (baseado nos recursos do TxScope) faça com que os métodos de negócio anotados com [Transaction] suportem transações distribuídas (entre duas bases de dados diferentes!).

Código Fonte

O código fonte deste artigo está disponível para download em: http://ericlemes.wikidot.com/local–files/dotnet-spring-pt3/SpringParte3.zip .