Boas práticas sobre exceções

June 13, 2010

O suporte a exceções se tornou comum nas linguagens de programação modernas. Este mecanismo permite tratar erros e situações inesperadas de forma muito elegante.

Sem esse recurso, as funções sinalizavam erros através do seu retorno ou pelo uso de alguma variável global, era mais ou menos assim:

const
  ERRO_DIVISAO_POR_ZERO = 0;

function Dividir(Dividendo, Divisor: Integer): Extended;
begin
  if Divisor = 0 then
    Result := ERRO_DIVISAO_POR_ZERO
  else
    Result := Dividendo/Divisor;
end;

Ou seja, convencionou-se um valor que ao ser retornado pela função, indica uma condição de divisão por zero.

Toda vez que a função for chamada, devemos testar esse valor. No final, nosso código vira algo como:

procedure Teste;
begin
  Retorno = FazAlgo;
  if Retorno = ERRO then
  begin
    ShowMessage('Ocorreu um erro');
    Exit;
  end;
end;

Eu estou lendo o livro “The Pragmatic Progammer – From journeyman to mater” (Em português, “O Programador Pragmático – De aprendiz a Mestre”). Hoje cheguei num ponto onde ele fala justamente do que eu escrevi acima. Vou transcrever o exemplo do livro:

if (socket.read(name) != OK) {
  retcode = BAD_READ;
}
else {
  processName(name);
  if (socket.read(address) != OK) {
    retcode = BAD_READ;
  }
  else {
    processAddress(address);
    if (socket.read(telNo) != OK) {
      retcode = BAD_READ;
    }
    else {
      // etc, etc...
    }
  }
}
return retcode;

Como diz o livro: “A lógica normal do seu programa pode acabar sendo totalmente obscurecida por tratamentos de erros”.
Usando exceções, isso poderia ser reescrito assim: (Novamente, transcrevendo código do livro)

retcode = OK;
try {
  socket.read(name);
  process(name);

  socket.read(address);
  processAddress(address);

  socket.read(telNo);
  // etc, etc
}
catch (IOException e) {
  retcode = BAD_READ;
  Logger.log("Error reading individual: " + e.getMessage());
}
return retcode;

Eu iria um pouco mais além: Repare que a rotina acima ainda usa códigos de retorno para sinalizar erros. Ou seja, quem estiver chamando ela, terá que fazer um tratamento de condições de erro semelhante ao que vimos antes, o que poderia acabar levando ao mesmo tipo de código confuso do primeiro exemplo do livro. Eu adotaria uma das duas estratégias seguintes:

  • Não capturar IOException. Ou seja, quando alguma daquelas chamadas gerar um IOException, a exceção poderia ser capturada pelo chamador da rotina. Ficaria mais ou menos assim:
    public void processIndividualData(Socket socket) {
      socket.read(name);
      process(name);
    
      socket.read(address);
      processAddress(address);
    
      socket.read(telNo);
      // etc, etc
    }
    
    public void teste() {
      Socket socket = ....
      try {
        processIndividualData(socket);
      }
      catch (IOException e) {
        // trata a exceção aqui
      }
    }
    
  • Encapsular IOException em alguma outra exceção. Ficaria assim:
    public void processIndividualData(Socket socket) {
      try {
        socket.read(name);
        process(name);
    
        socket.read(address);
        processAddress(address);
    
        socket.read(telNo);
        // etc, etc
      }
      catch (IOException e) {
        throw new OutraException(e);
      }
    }
    
    public void teste() {
      Socket socket = ....
      try {
        processIndividualData(socket);
      }
      catch (OutraException e) {
        // trata a exceção aqui
      }
    }
    

    Isso pode parecer desnecessario, mas vou explicar a importancia de esconder certas exceções:
    Imagine que você implementou um DAO (Data Access Object) para abstrair a persistencia de objetos Cliente no seu sistema. Você teria uma interface assim:

    public interface ClienteDao {
      Cliente[] listarTodos();
    }
    

    E digamos que você tenha uma implementação desse DAO que usa um banco de dados relacional como mecanismo de persistencia. Para acessar o banco de dados, utiliza JDBC.

    public class ClienteDaoJdbcImpl implements ClienteDao {
      public Cliente[] listarTodos() {
        // Implementação aqui
      }
    }
    

    As chamadas ao JDBC podem gerar exceções do tipo SQLException. Então o seu método ClienteDaoJdbcImpl vai repassar essa exceção para o chamador? Isso significa que o usuário do seu DAO saberá que o DAO é implementado para bancos de dados relacionais. O que aconteceria se você quisesse utilizar uma implementação da interface que usa arquivos Xml para persistir as informações? Não haveria nenhum SQLException mais, mas aí como ficaria o seu código que já conta com diversos tratamentos de SQLException onde o antigo DAO era utilizado? Uma das grandes vantagens do DAO é abstrair o mecanismo de peristencia (mas isso não é assunto para este post), só que esta vantagem foi perdida nesse caso. Para resolver este problema, podemos fazer algo assim:

    public class ClienteDaoJdbcImpl implements ClienteDao {
      public Cliente[] listarTodos() {
        try {
          // Implementação aqui
        }
        catch (SQLException e) {
          throw new DaoException(e);
        }
      }
    }
    

    Assim, não importa o que a implementação do DAO faça, você irá sempre se preocupar apenas em tratar DaoException.

Como vimos, o uso de exceções ajuda a melhorar a clareza do nosso código ao separar o tratamento de erros do fluxo normal do código.

Para finalizar, algumas recomendações:

  • Jamais “engula” exceções. Se você capturou uma exceção e não é capaz de trata-la, repasse-a para o chamador
  • Tenha um conjunto rico de exceções.
  • Exceções de baixo nível não devem ser passadas para camadas de mais alto nível. É o exemplo do ClienteDao acima.
0

Exemplo real de script de build para Delphi

June 6, 2010

No último post da série sobre integração contínua com Hudson e Delphi, mostrei como criar um script para automatizar o build de projetos Delphi. Aqui vou mostrar um exemplo real, que utilizo para compilar meus componentes.

@echo off
call "%D2010_ROOT%\bin\rsvars.bat"

echo ******************************
echo *** Compilando componentes ***
echo ******************************
msbuild Src\Componentes.dproj /p:config=Release
if %ERRORLEVEL% neq 0 Exit /b 1

echo ****************************************
echo *** Compilando pacote de design time ***
echo ****************************************
msbuild Src\ComponentesDsn.dproj /p:config=Release

Aqui não coloquei o caminho para o Delphi diretamente no script, ao invés disso eu faço referência através de uma variável de ambiente que eu criei, chamada D2010_ROOT. Na minha máquina, ela aponta para “C:\Arquivos de programas\Embarcadero\RAD Studio\7.0\”, sendo que eu posso rodar esse mesmo script em uma máquina com o Delphi instalado em outro caminho (Por exemplo, em um Windows em inglês, onde haverá “Program files” ao invés de “Arquivos de programas”), bastando ter a variável de ambiente declarada no Windows com o valor apropriado.

Isso abre espaço para uma recomendação: Nunca trabalhar com caminhos fixos nos projetos, pois isso pode eventualmente trazer problemas no projeto. Exemplo: Meus projetos Delphi estão todos localizados em “D:\Desenvolvimento\Delphi\Projetos\”, mas eu não posso ter esse caminho hard-coded dentro do meu projeto (mesmo que eu decida adotar o padrão de sempre trabalhar com essas pastas em qualquer máquina onde eu esteja) pois, por exemplo, isso me traria problemas com o Hudson (ele faz o check-out do projeto em uma pasta à escolha dele, ele não segue o seu padrão).

2

Integração contínua com Hudson e Delphi – Parte III

June 4, 2010

Finalmente chegamos na terceira – e última – parte da série “Integração contínua com Hudson e Delphi”.

Agora que já temos algum conhecimento básico do assunto e as ferramentas necessárias já estão instaladas, podemos finalmente colocar tudo em funcionamento.

Para poupar tempo, eu criei um projeto em Delphi sobre o qual poderemos trabalhar. Eu fiz em Delphi 2010, mas provavelmente não haverão grandes problemas em abrir em outras versões do Delphi (Quem encontrar alguma dificuldade pode entrar em contato comigo). Baixe o projeto aqui e descompacte-o na pasta DemoIC que você criou na segunda parte da série.

A integração contínua precisa que o build do projeto seja automatizado, e no momento nós não temos isso. Precisamos de um meio simples de executar o build em um único passo. Felizmente, o Delphi sempre teve um compilador de linha de comando, que poderia ser utilizado justamente para isso, e melhor ainda: Desde a versão 2007 ele trabalha em conjunto com o MSBuild, que é uma ferramenta de automação de builds da Microsoft, que torna o processo ainda mais fácil. Então tudo o que precisamos fazer é criar um script .bat que faça isso. Vamos lá:

  1. Crie um arquivo chamado Build.bat na sua pasta “DemoIC”.
  2. Abra o arquivo no Bloco de Notas e digite o seguinte código

    @echo off
    call "C:\Arquivos de programas\Embarcadero\RAD Studio\7.0\bin\rsvars.bat"
    msbuild "Src\Calculadora.dproj" /p:configuration="Release"

  3. Salve o arquivo

Com isso, temos uma forma simples de executar o build do nosso projeto. Vamos explicar o conteúdo deste arquivo:

  • A primeira linha serve para evitar que os outros comandos do script fiquem aparecendo na tela. É apenas uma questão visual, não interfere no funcionamento do script.
  • A segunda linha chama um arquivo .bat que acompanha o Delphi. Ele ajusta algumas configurações antes que possamos chamar o MSBuild. Eu recomendo que você crie uma variável de ambiente no computador que aponte para a pasta de instalação do Delphi, e edite o script de build para invocar o rsvars.bat com base nesta variável. Isso permite que o mesmo script de build possa ser utilizado em diferentes máquinas, cada uma com o Delphi instalado em um lugar diferente.
  • A terceira linha chama o MSBuild para finalmente compilar o projeto. Repare no parâmetro /p:configuration=”Release”. Você deve saber que os projetos no Delphi podem ter diferentes configurações de build, e este parâmetro serve justamente para especificarmos qual configuração será utilizada.

Agora, vamos adicionar estes arquivos no nosso sistema de controle de versão. Para isso, vá até a pasta DemoIC, selecione todos os arquivos e pastas exceto as pastas DCU, DCUTestes, Out e OutTestes. Clique com o botão direito do mouse em algum dos arquivos ou pastas que esteja selecionado, vá ao menu “TortoiseSVN”, e depois em “Add”.

Deve surgir uma tela como esta:

Remova a seleção das seguintes pastas e arquivos, pois eles não devem ser versionados:

  • DemoCalculadora.groupproj.local
  • Src/_history
  • Src/Calculadora.dproj.local
  • Src/Calculadora.identcache
  • SrcTestes/__history
  • SrcTestes/CalculadoraTests.dproj.local
  • SrcTestes/CalculadoraTests.identcache

Feito isso, clique “OK”. Surgirá uma tela mostrando os arquivos adicionados, e clique “OK” nela tambem. Após isso, clique com o botão direito em alguma área livre da pasta “DemoIC” e clique em “SVN Commit”.

Na tela que se abre, clique “OK”, e depois “OK” na tela seguinte. Pronto, agora todos os fontes do nosso projeto estão no Subversion.

Agora vamos criar uma tarefa no Hudson. Criar uma tarefa é como cadastrar o projeto no Hudson para que este controle o build do projeto.

Vamos lá, abra o Hudson. A tela inicial dele deve ser a seguinte:
Tela inicial do Hudson

Localize, no lado esquerdo da tela, o item “Nova Tarefa” e clique nele.

Você deve ver esta tela agora:

No campo “Nome da tarefa”, informe “DemoIC” (sem as aspas).
Marque o item “Construir um projeto de software free-style”. Isso nos permitirá configurar livremente a rotina de build do projeto. O Hudson conta com outras opções, que não serão utilizadas aqui.

Muito bem, agora clique “OK”.

Na tela seguinte, temos que informar mais alguns parâmetros do projeto. Primeiro, temos que dizer qual servidor de gerenciamento de código fonte será utilizado: Então, localize o item “Gerenciamento de Código Fonte” e selecione “Subversion”.
Ao selecionar “Subversion”, alguns novos campos aparecerão na tela, vamos então configura-los: No campo “URL do Repositório”, informe “https://localhost/svn/DemoIC/trunk” (sem aspas), esse é o endereço para o repositório do projeto, contendo tambem o caminho da pasta onde estão os fontes que o Hudson irá baixar.
Em “Diretório do módulo local (opcional)”, informe “DemoIC”. Quando o Hudson faz o download dos arquivos do repositório, ele irá colocar os arquivos em uma pasta com o nome que foi especificado aqui. Caso não informemos nenhum valor, ele irá utilizar o último nome da URL do repositório, que no nosso caso seria “trunk”.

Muito bem, agora já informamos ao Hudson como ele fará para obter o código fonte do projeto. Agora precisamos informa-lo como ele fará para compilar o projeto. Para isso, localize um combo escrito “Add build step” e clique nele. Será aberto um menu com as seguintes opções:

  • Executar shell
    Instrui o Hudson a executar um comando de shell do Linux
  • Invocar alvos Maven de alto nível
    Faz com que o Hudson invoque o Maven para realizar o build
  • Executar comando do Windows
    Semelhante à primeira opção, mas serve para ser utilizada no Windows.
  • Invocar ant
    Faz com que o Hudson invoque o Ant para realizar o build

Utilizaremos a terceira opção. Após seleciona-la, deve surgir na sua tela um campo como este:

Este campo funciona da seguinte forma: O que você digitar nele, será gravado pelo Hudson em um arquivo .bat que estará localizado em uma pasta um nível acima da pasta “DemoIC” na qual o Hudson colocará os fontes do projeto ao construir o mesmo. Esse arquivo .bat criado pelo Hudson será executado quando for o momento de realizar algum build. Então o que temos que digitar ali é apenas o código necessario para executar o arquivo Build.bat dentro da pasta DemoIC. Então apenas digite o seguinte:

cd DemoIC
call Build.bat

Finalmente, clique em “Salvar”.

Para testar se está tudo correto, clique em “Construir Agora”, isso deverá iniciar um build do projeto.

Se tudo foi feito corretamente, você deverá ver uma tela como esta:

Repare na cor da bolinha no “Histórico de Construção”. Azul indica um build bem sucedido, enquanto vermelho indica o contrário.

Mas precisamos automatizar ainda mais o processo de build, pois isto deve ocorrer a cada commit do código fonte. Felizmente, o Hudson nos dá um mecanismo simples e eficiente de dispararmos o build de forma automatizada. Basta acessar a URL http://localhost:8080/hudson/job/DemoIC/build. Não é necessario nenhuma outra ação, apenas dar um GET nesta URL já faz com que um build seja disparado.

O Subversion nos dá outro recurso que será fundamental neste momento, são os chamados hooks. Hooks são comandos que você pode programar o Subversion para executar automaticamente em determinadas situações. No nosso caso, vamos utilizar um post-commit hook, onde poderemos definir uma ação a ser executada sempre após um commit.

Mas ainda falta uma peça neste quebra-cabeças: Como disparar um GET naquela URL do Hudson que mostrei anteriormente? Para fazer isso, eu uso esta ferramenta. Se preferir, aqui está um link direto para download.

Tendo baixado a ferramenta, coloque-a em uma pasta de sua preferência e renomeie o arquivo para wget.exe caso ele tenha vindo com outro nome. Não é necessário instalar. Após isso, coloque a pasta que você escolheu no path do Windows e reinicie o computador.

Muito bem, agora vamos configurar o hook:

  1. Abra o VisualSVN Server Manager.
  2. No painel da esquerda, localize o item “DemoIC” que está localizado um nivel abaixo do item “Repositories”.
  3. Clique com o botão direito em “DemoIC” e vá em “Properties”.
  4. Na tela que se abre, vá na aba “Hooks”.
  5. Clique sobre “Post-commit hook”.
  6. Clique em “Edit”.
  7. Na tela que se abre, digite o seguinte:
    wget http://localhost:8080/hudson/job/DemoIC/build --spider
  8. Clique OK
  9. Clique OK novamente e feche o VisualSVN

Obs: O parâmetro

--spider

serve para que o wget não faça o download da URL especificada, mas sim apenas uma requisição GET no servidor. Isso evita tráfego de rede desnecessário.

Podemos fazer um teste agora. Abra o Delphi, e edite qualquer arquivo fonte do projeto de forma a introduzir um erro de compilação. Salve o projeto, vá na pasta e dê um commit no Subversion. Agora acesse o Hudson e clique na tarefa “DemoIC” para exibir as informações do nosso projeto. Neste momento, você deve estar vendo no “Histórico de construção” com uma bolinha vermelha, isto corresponde ao build que foi iniciado em função do commit que você acabou de fazer no Subversion. Agora vá no Delphi, desfaça a edição que você fez anteriormente e dê um novo commit.

Após isso, olhe novamente no Hudson, na página de informações do projeto “DemoIC” e você deve ter notado um terceiro item no “Histórico de construção”, desta vez sinalizando um build bem sucedido.

Bom, está quase tudo pronto. Mas ainda falta fazer com que o build teste o projeto, desta forma um build bem sucedido não será simplesmente um build sem erros de compilação, mas sim um build onde tudo foi compilado com sucesso E onde todos os testes automtizados passaram. O que precisamos fazer é editar no nosso Build.bat de forma a faze-lo compilar e executar o projeto de testes unitários, então vamos lá:

  1. Abra o arquivo Build.bat no bloco de notas.
  2. Acrescente o seguinte código ao final do arquivo

    if %ERRORLEVEL% neq 0 Exit /b 1
    msbuild "SrcTestes\CalculadoraTests.dproj" /p:configuration="BuildAutomatizado"
    if %ERRORLEVEL% neq 0 Exit /b 1
    call OutTestes\CalculadoraTests.exe

    A primeira linha serve para terminar o script sinalizando uma condição de erro caso o passo anterior (isto é, a compilação do projeto principal) não tenha sido bem sucedido. A segunda linha irá compilar o projeto de testes unitários, e a terceira linha irá terminar o script sinalizando uma condição de erro caso a compilação dos testes unitários não tenha sido bem sucedida. Por fim, a última linha executa os testes unitários.

Note que para compilar os testes unitários, utilizei uma configuração de build chamada “BuildAutomatizado”, isso por que o projeto de testes unitários precisará ser compilado com algumas características especiais para poder ser efetivamente utilizado no nosso script de build, a saber:

  • Precisa ser um aplicativo console.
  • O aplicativo precisa retornar zero caso todos os testes passem, e retornar qualquer outro valor caso contrário.

Vamos então criar esta configuração. Abra o projeto CalculadoraTests no Delphi e crie uma nova configuração de build (Build configuration) para ele. Chame-a de “BuildAutomatizado”. No Delphi 2010, isso é feito no seguinte menu:

Agora vá nas opções da configuração BuildAutomatizado e defina os seguintes parâmetros:

  • Conditional defines: CONSOLE_TESTRUNNER
  • Unit output directory: ..\DCUTestes\
  • Output directory: ..\OutTestes\

Agora abra o arquivo CalculadoraTests.dpr e modifique-o para ficar assim:


program CalculadoraTests;
{

  Delphi DUnit Test Project
  -------------------------
  This project contains the DUnit test framework and the GUI/Console test runners.
  Add "CONSOLE_TESTRUNNER" to the conditional defines entry in the project options
  to use the console test runner.  Otherwise the GUI test runner will be used by
  default.

}

{$IFDEF CONSOLE_TESTRUNNER}
{$APPTYPE CONSOLE}
{$ENDIF}

uses
  Forms,
  TestFramework,
  GUITestRunner,
  TextTestRunner,
  UCalculadoraTests in 'UCalculadoraTests.pas';

{$R *.RES}

var
  Results: TTestResult;

begin
  Application.Initialize;
  if IsConsole then
  begin
    Results := TextTestRunner.RunRegisteredTests;
    try
      if Results.FailureCount + Results.ErrorCount = 0 then
        ExitCode := 0
      else
        ExitCode := 1;
    finally
      Results.Free;
    end;
  end
  else
    GUITestRunner.RunRegisteredTests;
end.

Basicamente, essas mudanças no dpr foram apenas para fazer o processo retornar o código 1 caso algum teste não tenha passado, e retornar 0 caso todos os testes tenham passado.

Feito isso, salve tudo, faça um commit no Subversion e observe o DemoIC no Hudson, deve ter sido gerado um novo build bem sucedido lá.

Agora vamos simular uma falha nos testes. Abra o arquivo UCalculadora.pas e adicione um “Exit;” logo após o begin no método TCalculadora.Calcular(). Ele ficará assim:

function TCalculadora.Calcular(A, B: Extended; Operacao: TOperacao): Extended;
begin
  Exit;
  case Operacao of
    oAdicao: Result := A + B;
    oSubtracao: Result := A - B;
    oMultiplicacao: Result := A * B;
    oDivisao: Result := A / B;
  end;
end;

Salve, faça um commit e observe o DemoIC no Hudson. Você deverá ver um build falho agora. Se você clicar sobre o item correspondente a um build “Histórico de construção”, poderá acessar informações específicas sobre um determinado build. Por exemplo, em “Saída do console” você poderá visualizar o que foi gerado no console pelo script de build, isso é útil para identificar o ponto onde o build falhou.

Como pode ver, nós facilmente descobrimos um erro que foi introduzido “acidentalmente” no código, e então teremos a possibilidade de corrigi-lo rapidamente.

Uma coisa interessante, é que o nosso script de build (Build.bat) pode ser utilizado independente do Hudson. Se você mudar o servidor de integração contínua, poderá continuar utilizando. Ele serve inclusive para uso local. Exemplo: Você terminou uma implementação e quer ver se tudo está funcionando bem antes de enviar o código para o controle de versão? Basta executar o script de build.

Para terminar, farei algumas considerações que serão úteis quando você for utilizar na prática (eu espero que você o faça) o que leu aqui:
Eu instalei tudo em uma mesma máquina para simplificar as explicações, mas no mundo real não será assim. Considere que você possuirá os seguintes tipos de computadores no seu ambiente e eu vou explicar quais ferramentas seriam instaladas em cada um, dentre as que utilizamos nesta série:

  • Máquinas de desenvolvimento: Onde os programadores trabalharão. Aqui normalmente seria instalado o Delphi e o cliente do Subversion (No nosso caso, o TortoiseSVN).
  • Servidor de código fonte: Aqui é onde ficarão os fontes do projeto. Instale o servidor Subversion (No nosso caso o VisualSVN) e o WGet.
  • Servidor de integração contínua: É onde ficará instalado o Hudson.
  • Servidor de build: Onde os builds ocorrerão, precisa do Delphi instalado.

Sei que o Hudson suporta realizar builds em máquinas remotas, mas não sei configurar isso. Então para mim o servidor de integração e de build são um só. Pode ser interessante separar isso caso o seu sistema precise ser compilado/testado em diferentes plataformas. Assim você teria um servidor central de integração, que dispararia o build em diversas outras máquinas cada uma com uma plataforma diferente.

O servidor de integração e de código fonte podem ser na mesma máquina ou não, isso fica a seu critério.

O Hudson ainda tem muitos recursos. Por exemplo, ele pode ser configurado para enviar e-mails quando houver alguma mudança relevante no projeto. Ou seja, quando o projeto estava estável e de repente um build falhou, ou quando os builds não seram bem sucedidos e de repente um build passou a funcionar. Isso é importante para os membros da equipe saberem o estado do build sem terem que ficar monitorando o Hudson.

Ele tambem suporta plugins, para adicionar novas funcionalidades ao mesmo. A instalação de plugins é bem simples, dê uma olhada no menu “Gerenciar Hudson/Gerenciar plugins”.

Com isso, chegamos ao final desta série. Espero ter conseguido ser claro e mostrar as vantagens da prática da integração contínua e o quão simples é configurar um ambiente para começar a trabalhar desta forma.

5

Quais são as suas prioridades como desenvolvedor?

May 23, 2010

Quando você está codificando, quais são os fatores que você leva em consideração para decidir como implementar uma determinada situação?

Vamos clarear mais. Considere os seguintes pontos:

  • Legibilidade
  • Performance / Consumo de memória
  • Manutenabilidade (em outras palavras: O quão fácil ou difícil será dar manutenção naquele código futuramente)
  • Tempo de desenvolvimento
  • Quantidade de código

Peço que quem estiver lendo este post, dê uma pausa e faça uma lista destes items partindo do que você acha mais importante até o menos importante, antes de prosseguir.

Agora vamos discutir cada um:

  1. Quantidade de código
    Muitos programadores acreditam que uma solução é melhor que outra apenas por envolver menos linhas de código. Eu digo que este fator, por si só, está entre os menos importantes para mim. Pense: Que vantagens isso tras? Você paga imposto de acordo com o tamanho dos seus fontes? E de que adianta gastar menos tempo digitando o código, se toda vez que você precisar fazer alguma alteração você perder o dobro de tempo pra entender o que o código faz?
  2. Tempo de desenvolvimento
    O cliente está te cobrando a cada cinco minutos, o seu prazo já estourou, e então você implementa a coisa do jeito mais rápido possível, o que quase sempre significa ignorar toda e qualquer boa-prática.

    Ok, mas saiba toda vez que você precisar dar manutenção no código, vai levar mais tempo do que levaria se tivesse gasto um pouco mais de tempo no inicio para fazer algo melhor. Então eu pergunto: Será que o lucro de hoje não se transformará em uma série de prejuízos amanhã? Não seria melhor ter um prejuízo hoje para ter uma série de lucros amanhã?

    Se situações assim são frequentes, sugiro maior atenção na hora de definir prazos. É melhor dar um prazo de 20 dias e entregar o produto em 15, do que dar um prazo de 10 dias e entregar o produto nos mesmos 15 dias. Sempre inclua nos seus prazos um tempo a mais para cobrir imprevistos.

  3. Performance / Consumo de memória
    Sério, vamos deixar as previsões sobre o futuro com os místicos, ok? Não há nada pior do que o programador que fica fazendo previsões sobre se uma determinada rotina ficará muito pesada, pois é bem provável que nossos palpites não causem nenhuma melhora significativa no produto. Estamos na era dos gibabytes de memória RAM, processadores com diversos núcleos, etc, então não adianta muito gastar horas/dias otimizando rotinas para salvar alguns quilobytes ou megabytes de memória RAM, ou alguns ciclos de processamento, por que isso não fará absolutamente nenhuma diferença para o usuário final.
    Se for comprovado, na prática, que uma determinada parte do sistema está consumindo recursos de forma realmente excessiva, você deve encontrar o gargalo com a ajuda de profilers, e só então você dará um jeito de resolver o gargalo.

    Leve em consideração o Princípio de Pareto, que diz que 80% dos problemas se originam em apenas 20% das causas. Trazendo isso para o assunto em questão aqui, podemos dizer 20% do código do seu sistema é responsável por 80% do tempo de execução do mesmo. Ou seja: Por mais que você otimizar esses 80% de código restante, o ganho no final das contas será mínimo.

  4. Manutenabilidade
    Pense no quão facil ou dificil será dar manutenção no código que você está escrevendo. Veja não pelo seu ponto de vista, mas no de algum outro desenvolvedor que eventualmente ficará responsavel por manter este código. De que adianta fazer algo rapidamente hoje, se você se vir no inferno cada vez que precisar alterar algo no código (e isso significa perder muito tempo)? Talvez seja necessário até mesmo refazer tudo, então teria sido melhor fazer algo bom desde o inicio.
  5. Legibilidade
    Quando codificar uma rotina, imagine-se tendo que entende-la depois de um ano inteiro sem olhar para ela. Imagine outro desenvolvedor tendo que entende-la. O seu código deve ser fácil de ler nestas situações

Quando priorizamos performance, quantidade de linhas de código e tempo de desenvolvimento, a tendência é uma redução na legibilidade e na manutenabilidade. Acho que um bom profissional deve ser capaz de balancear tudo isso da melhor forma possível, mas nunca deixando de lado a preocupação com a qualidade do projeto. Muitos dizem que o cliente não vê o código fonte e que o importante é o produto funcionando. Eu não discordo, mas temos que considerar o seguinte: O produto pode estar funcionando e atendendo com maestria as necessidades do cliente, mas se o projeto não foi estruturado de uma maneira saudavel, então essa qualidade deixará de existir cedo ou tarde devido às dificuldades de manter e expandir o projeto e o produto não mais atenderá às necessidades do cliente.

Agora vos apresento a minha lista de prioridades:

  1. Legibilidade
    Se o código não é legível, não poderei trabalhar com ele e entregar um produto de qualidade.
  2. Manutenabilidade
    Se tenho dificuldades de manter o código já escrito, estarei sempre estourando prazos, entregando requisitos mal implementados, bugs, etc.
  3. Performance
  4. Tempo de desenvolvimento
  5. Quantidade de código

Isso quer dizer que eu não me importo em escrever centenas ou milhares de linhas de código? De forma alguma, só quer dizer que eu não abro mão de fatores mais importantes apenas para economizar linhas de código. Mas se for possível escrever menos, sem prejudicar a legibilidade ou a manutenabilidade, é claro que eu prefiro!

Como eu disse antes, se for constatado que o sistema está consumindo muitos recursos, utilizamos um profiler para identificar o gargalo e trabalhamos para resolver este gargalo. Nesse caso, possivelmente terei que abrir mão de fatores mais importantes como a legibilidade do código pois, como eu disse antes, o que importa é ter um produto de qualidade que satisfaça ao cliente.

2

Integração contínua com Hudson e Delphi – Parte II

May 20, 2010

Na primeira parte desta série, apresentei o conceito de integração contínua, mas me mantive apenas na teoria. Agora vamos preparar o terreno para colocarmos a idéia em prática instalando as ferramentas necessárias.

Em primeiro lugar, precisamos de um tipo de software conhecido como servidor de integração contínua. Eu utilizo o Hudson, um servidor escrito em Java mas que pode trabalhar com projetos escritos em qualquer linguagem. Este servidor é que será responsável obter o código fonte do projeto no servidor de controle de versão, iniciar o build, manter um histórico dos builds bem e mal sucedidos, etc.

Sendo escrito em Java, então já sabemos que vamos precisar também da JVM. Como este sistema usa a tecnologia JEE, precisaremos também de um servidor de aplicações. O Apache Tomcat dá conta do recado.

Então a primeira coisa é baixar e instalar o Java SDK e o Apache Tomcat.

Nos dois casos, a instalação segue o modelo NNF (Next, Next, Finish), então não vou entrar em detalhes aqui.

Obs: Daqui para frente, irei considerar que o Tomcat foi instalado em “C:\Arquivos de Programas\Apache Software Foundation\Tomcat 6.0\” e que os valores padrão da instalação foram mantidos, ou seja, a porta utilizada será 8080

O Tomcat é instalado por padrão como serviço. Eu tive alguns problemas para o Hudson compilar meus projetos Delphi quando o Tomcat está rodando como serviço, então passei a utiliza-lo como aplicativo mesmo. Para fazer isso, primeiro temos que desativar o serviço Tomcat:

  1. Vá até o gerenciador de serviços do Windows, localize o serviço “Apache Tomcat 6″ (Ou seja lá qual versão que você instalou)
  2. Clique com o botão direito do mouse sobre o serviço e clique em “Parar”
  3. Aguarde até o serviço ser finalizado
  4. Clique com o botão direito do mouse sobre o serviço e clique em “Propriedades”
  5. Em “Tipo de inicialização”, selecione “Desativado”
  6. Clique em “OK” e pronto

Agora precisamos de um meio para que o aplicativo Tomcat seja iniciado automaticamente junto com o Windows.
O jeito mais simples é colocar um atalho para “C:\Arquivos de Programas\Apache Software Foundation\Tomcat 6.0\bin\tomcat6.exe” na pasta “Inicializar” do Menu Iniciar do Windows.

Uma vez que o Tomcat já esteja pronto, podemos instalar o Hudson nele. Para isso, baixe-o aqui.

Coloque o arquivo hudson.war que você baixou na pasta “webapps” onde está instalado o Tomcat.

Provavelmente o Tomcat não estará rodando agora. Então execute o arquivo “C:\Arquivos de Programas\Apache Software Foundation\Tomcat 6.0\bin\tomcat6.exe” para coloca-lo em execução.

Se tudo correu bem, você agora poderá acessar o Hudson por este endereço: http://localhost:8080/hudson/

Mas antes de podermos efetivamente utiliza-lo, devemos ter um projeto configurado em algum servidor de controle de versão. Por padrão, o Hudson vem com suporte para CVS e Subversion. Sei que existe um plugin que adiciona suporte a Starteam. Creio que existam plugins para diversos outros sistemas de controle de versão também.

Irei utilizar aqui o Subversion. Então devemos agora configurar um servidor Subversion, e no nosso caso será utilizado o VisualSVN que pode ser baixado aqui. A instalação dele tambem é simples, apenas mantenha os valores padrão que o instalador sugere e vá dando Next para prosseguir.

Ao final da instalação, o VisualSVN Server deve ter sido aberto. A tela dele é essa aqui:
Tela inicial do VisualSVN Server

Agora vamos criar dois usuários no servidor: Um para nós mesmos, e outro para o Hudson utilizar quando for necessario baixar os fontes do servidor. Para isso, você deve localizar a pasta “Users” que fica no painel à esquerda na tela do VisualSVN. Clique com o botão direito nela, e depois em “Create User”. Informe nome de login, senha, repita a senha, e clique em “OK”. Irei utilizar como nome de usuários “magno” e “hudson”, sendo as senhas iguais aos respectivos nomes. Após fazer isso, a tela do seu VisualSVN deve estar semelhante a esta:
Tela do VisualSVN com os usuários criados

Precisamos ainda criar um repositório, que é o local onde os fontes do nosso projeto ficam localizados. Para isso, localize o item “Repositories” na tela principal do VisualSVN, clique com o botão direito e depois em “Create New Repository”. Na tela que se abre, digite o nome para o repositorio (irei utilizar “DemoIC”) e clique check box que diz “Create default structure (trunk, branches, tags)” e depois em “OK”. Habilitar este check-box faz com que sejam criadas três pastas no nosso repositório chamadas “trunk”, “branches” e “tags”, embora elas não sejam um pre-requisito para o funcionamento nem do Subversion nem do Hudson, seu uso constitui uma boa prática e por isso incentivo seu uso aqui. O propósito de cada uma destas pastas está além do escopo deste artigo, então não vou me aprofundar aqui).

Neste momento, o VisualSVN deve estar assim:
Tela do VisualSVN com o repositório criado

Agora que já temos o servidor Subversion rodando, repositório criado e Hudson em execução, podemos começar a trabalhar nosso projeto em Delphi. Mas para sermos capazes de usar o Subversion para gerenciar os fontes deste projeto, vamos precisar de um cliente para o mesmo. Eu uso o TortoiseSVN, que é gratuito e se integra ao Windows. Então baixe-o aqui e então execute o instalador. Mantenha as opções padrão e vá avançando. Ao final, você terá que reiniciar o computador.

Muito bem, vamos começar a trabalhar no nosso projeto em Delphi (vou partir do princípio que ele já está instalado, ok?).

Então agora crie no seu computador uma pasta chamada DemoIC, pode ser em qualquer lugar. Esta será a sua pasta de trabalho, onde você poderá trabalhar no projeto.

Agora abra a pasta, clique como botão direito em qualquer área livre dela e clique em “SVN Checkout”. Em “URL of repository”, coloque “https://localhost/svn/DemoIC/trunk” (sem as aspas) e clique “OK”. Provavelmente aparecerá uma janela como esta:
Clique em “Accept permanently”.

Após isso, será aberta a seguinte tela:

Esta tela está pedindo um nome de usuário e senha para se autenticar no servidor Subversion. Informe o nome de usuário e senha que você cadastrou no VisualSVN (mas não o usuário destinado ao hudson). Se quiser, marque o checkbox “Save authentication” para que você não precise informar estes dados sempre que fizer algo no repositório.

Por hoje ficaremos por aqui. O objetivo desta parte da série era apenas mostrar a instalação e configuração de toda a infra-estrutura necessária para por em prática a integração contínua com projetos Delphi. No próximo, vamos finalmente implementar um projeto de demonstração em Delphi e ver o Hudson em funcionamento.

1

Integração contínua com Hudson e Delphi – Parte I

May 13, 2010

Após algum tempo ausente por falta de tempo, estou de volta com o blog.
Para marcar o retorno, estou abrindo uma série de artigos na qual pretendo falar de uma prática chamada Integração Contínua. Inicialmente explicando o que é, seus benefícios, e por fim como utiliza-la na prática através de ferramentas como Subversion, Delphi, DUnit e Hudson.

Nas palavras de Martin Fowler:
“Integração contínua é uma prática de desenvolvimento de software onde os membros de uma equipe integram seu trabalho frequentemente, geralmente cada um integra no mínimo diariamente – levando a múltiplas integrações por dia. Cada integração é verificada por um build automático (incluindo testes) para detectar erros de integração tão rápido quanto possível. Muitas equipes descobrem que esta prática leva a problemas de integração significativamente reduzidos e permite que a equipe desenvolva software coeso mais rapidamente.”

Imagine o seguinte:

  1. O desenvolvedor A faz um check-out do repositório do projeto e começa a trabalhar em alguma tarefa.
  2. O desenvolvedor B faz um check-out e começa a trabalhar em outra tarefa.
  3. O desenvolvedor B conclui a sua tarefa, executa os devidos testes, verifica que tudo está funcionando corretamente e então envia suas alterações para o repositório.
  4. O desenvolvedor A também conclui o que estava fazendo, executa os devidos testes, verifica que tudo está funcionando corretamente e então envia suas alterações para o repositório.

Que maravilha, todo mundo concluiu suas tarefas, e todo mundo testou seu código para saber que está funcionando, então o projeto continua estável como uma rocha.
Será? Bom, alguem lembrou de testar a base de código contendo as alterações do desenvolvedor A junto com as alterações do desenvolvedor B? O que acontece caso o desenvolvedor B tenha alterado algum método que o desenvolvedor A utilizou? O nosso sistema em um minuto passa de estável como uma rocha para estável como um terremoto.

A pior parte é que provavelmente só saberíamos disso quando alguem baixasse novamente o código fonte e tentasse compilar ou executar os testes. Quando tempo pode ter se passado? Horas, talvez dias. Aí seria mais difícil rastrear a origem do problema e corrigir.

Você pode argumentar: Bom, é só determinar que todo mundo atualize a sua base de código e execute todos os testes antes de enviar suas alterações para o servidor. Certo, isso é importante, e isso deve ser feito independente de a equipe adotar ou não integração contínua. Mas será que aquele programador que passou a tarde inteira corrigindo um bug no cálculo de folha de pagamento, que terminou a correção às 21h de uma sexta-feira, vai lembrar de fazer isso? Ele vai testar apenas o seu código, quando muito!

A Integração Contínua vem determinar justamente que todo o projeto seja integrado freqüentemente. Assim, erros que surjam em situações como a dos nossos colegas Desenvolvedor A e Desenvolvedor B, serão percebidos o mais rápido possível

Com integração contínua, o projeto inteiro deve ser construído e testado a cada check-in no repositório! Desta forma, problemas de integração como o relatado aqui serão notados muito rapidamente após os check-ins, e então o desenvolvedor responsável poderá fazer as devidas alterações.

Para que tudo isso funcione bem, o Sr. Fowler definiu algumas recomendações. Irei resumi-las aqui:

  • Mantenha um repositório dos fontes
    Use uma ferramenta como Subversion, Git ou outra do gênero para manter um repositório central com os fontes do seu projeto. Coloque tudo você julgar necessário para construir o projeto: Script de banco de dados, script para instalador, etc. Em geral, o conteúdo do repositório deve tudo o que é necessario para construir e testar o projeto em uma máquina zerada, mas obviamente algumas coisas são impraticaveis para manter no repositorio: Compilador, máquina virtual, etc. Então, use o bom senso! O que é realmente importante é que seja simples dar um check-out do projeto e construi-lo em uma máquina nova. Tudo que estiver sujeito a mudanças, que não puder ser construído automaticamente, deve estar no repositório.
  • Automatize o build
    Faça com que seja possível construir o projeto com um único comando, sem intervenções humanas. Existem diversas ferramentas que auxiliam nisso: Make, Ant, NAnt, Maven, MSBuild.
    Desde o Delphi 2007, o MSBuild é utilizado para controlar os builds. Compilar nossos projetos Delphi via linha de comando é tão simples quanto digitar “msbuild C:\Projetos\ProjetoX\Fontes\ProjetoX.dproj” no prompt de comando. Em meus projetos, eu mantenho um arquivo batch ou powershell que invoca o msbuild e executa alguns passos adicionais (como por exemplo executar os testes unitários).
  • Torne seu build auto-testavel
    Basicamente, é incluir a execução de testes automatizados no processo de build, como eu disse no item anterior.
  • Faça check-ins frequentes
    Fazer o check-in não é apenas mandar nossas alterações para o repositorio: Primeiro devemos atualizar nossa cópia de trabalho local para receber as novas alterações do repositório, depois executar o build (isso inclui os testes) na nossa máquina, e então (somente se tudo passar) fazer o check-in. Fazendo isso com freqüência, podemos encontrar erros mais rapidamente.
  • Cada check-in deve construir o projeto em uma máquina de integração
    Em poucas palavras: O processo que eu disse que cada desenvolvedor deve fazer antes de dar um check-in deve tambem ser feito automaticamente, a cada check-in, em uma máquina destinada especificamente a este proposito. Isso parece redundante, mas é necessario basicamente por dois motivos: O desenvolvedor pode simplesmente se esquecer de fazer isso na sua própria máquina. Além disso, diferenças no ambiente de desenvolvimento de cada desenvolvedor podem fazer com que algo funcione na máquina dele, mas falhe na máquina de integração. Veremos adiante como automatizar este processo de build a cada check-in.
  • Mantenha o build rápido
    A idéia da integração contínua é nos dar um feed-back rápido sobre falhas no projeto. Um build demorado só iria atrapalhar isso. Martin Fowler coloca um biuild de até 10 minutos como aceitável, mas obviamente quanto mais rápido conseguirmos melhor. E aqui build se refere não só ao processo de gerar os executaveis, mas sim todo o processo de transformar o código fonte no produto final destinado aos usuários, ou seja: Gerar documentação, instalador, executar testes automatizados, etc. Para acelerar esse processo, testes bem escritos ajudam e muito: Por exemplo, use e abuse de test-doubles para evitar acessar recursos custosos durante os testes unitários, pois isso melhora a confiabilidade dos testes e tambem a velocidade de execução dos mesmos. Se necessário, invista também em hardware tanto para a máquina de build quanto para a(s) máquina(s) onde os testes serão executados (pode ser que build e teste sejam feitos na mesma máquina, mas podem haver casos em que não seja assim).
  • Teste em um ambiente idêntico ao ambiente de produção
    O ambiente onde os testes serão executados deve ser o mais próximo possível do sistema onde o sistema será executado. É claro que nem sempre é viável testar em cada possível configuração, nesse caso devemos aplicar o bom senso: Dependendo das necessidades do projeto, pode ser mais ou menos provável que ele será sensível a diferentes versões de sistema operacional, servidor de banco de dados, etc, e estes parâmetros deverão ser usados para determinar em qual(is) ambiente(s) os testes deverão ser executados.
  • Faça com que seja simples para qualquer um obter o executável mais recente do projeto
    Deve haver um local definido onde qualquer um pode ir para buscar a última versão estável do projeto. Isso é importante para uso em demonstrações, testes, etc.
  • Todos devem saber o que está acontecendo
    Deve ser possível para qualquer um saber da situação do build: O último build foi bem sucedido? Houve algum problema? Qual?
  • Automatize o deploy
    Com integração contínua, você poderá se ver na necessidade de mover o sistema da máquina de build para a(s) máquina(s) de testes.
    Bom, se você pode fazer isso, uma conseqüência é que poderá também fazer o deploy automático para o ambiente de produção.

Na próxima parte do artigo começarei a abordar a parte prática, apresentando e instalando as ferramentas necessárias para implementar o ambiente de integração contínua.

6

Singleton: Lobo em pele de cordeiro, ou um pattern injustiçado?

March 20, 2010

Singleton é um design pattern (ou um anti-pattern, para alguns) que determina que só deve haver uma instancia de uma determinada classe em todo o sistema.

Há bastante controvérsia a respeito deste padrão, uns alegam que trata-se de um anti-pattern, isto é, algo que deve ser evitado. Enquanto os design-patterns são maneiras conhecidas e elegantes de resolver problemas recorrentes, os anti-patterns são maneiras que você NÃO deve utilizar para resolver problemas.

De fato, singletons, dependendo da maneira como são utilizados, podem gerar problemas com testes unitários, pois você não conseguirá alcançar o efeito de não deixar rastros do seus testes. Também não vai conseguir substitui-lo por um test-double, o que seria desejaval em alguns caso, como por exemplo se esse singleton acessa um arquivo no HD.

O problema nem está no fato de haver apenas uma instância do objeto. Em alguns casos isso é realmente necessário. O problema está na forma como esta instância é obtida.

Imagine a seguinte situação:

Seu sistema tem uma classe chamada TConfiguracoes que encapsula o acesso aos parâmetros de configuração do sistema, que são armazenados em um arquivo .ini junto à sua aplicação ou um banco de dados. Tal classe utiliza o padrão singleton, pois de fato não há sentido em ter mais de um objeto desta classe no sistema.

Você implementou a classe da seguinte forma:

unit UConfiguracoes;

interface

type
  IConfiguracoes = interface
    // guid
    // métodos de acesso aos parâmetros aqui
  end;

function Configuracoes: IConfiguracoes;

implementation

type
  TConfiguracoes = class(TInterfacedObject, IConfiguracoes)
  end;

var
  _Configuracoes: IConfiguracoes;

function Configuracoes: IConfiguracoes;
begin
  if not Assigned(_Configuracoes) then
    _Configuracoes := TConfiguracoes.Create;

  Result := _Configuracoes;
end;

end.

E usa da seguinte forma:

type
  IXXXService = interface
    // guid

    procedure FazAlgo;
  end;

  TXXXService = class(TInterfacedObject, IXXXService)
  private
    procedure FazAlgo;
  end;

implementation

{ TXXXService }

procedure TXXXService.FazAlgo;
begin
  if Configuracoes.ParametroY then
    FazIsso
  else
    FazAquilo;
end

Digamos que esta classe de configurações lê os seus parametros em um banco de dados. Durante os testes automatizados você terá que habilitar um banco de dados para esta classe poder funcionar, e correrá o risco de um teste enxergar mudanças feitas por testes anteriores, o que é algo ruim conforme eu expliquei anteriormente. É possível evitar isso se você zerar os dados do banco a cada teste, mas isso tornaria a execução dos mesmos mais demorada. Além do mais, seria uma configuração a mais para você se preocupar.

É desta forma que o singleton atrapalha os testes automatizados, sendo por isso tão criticado por alguns.

Como eu disse, o problema está na forma como a instância do singleton é obtida. Qual seria a forma mais apropriada então? Espero que a resposta seja obvia para quem leu este post. Para quem não leu, explico: Basta utilizar injeção de dependências. A nossa classe TXXXService ficaria assim:

type
  TXXXService = class(TInterfacedObject, IXXXService)
  private
    FConfiguracoes: IConfiguracoes;
    procedure FazAlgo;
  public
    constructor Create(const Configuracoes: IConfiguracoes);
  end;

implementation

{ TXXXService }

constructor TXXXService.Create(const Configuracoes: IConfiguracoes);
begin
  FConfiguracoes := Configuracoes;
end;

procedure TXXXService.FazAlgo;
begin
  if FConfiguracoes.ParametroY then
    FazIsso
  else
    FazAquilo;
end;

E para utilizar esta classe, seria assim:

var
  Service: IXXXService;
begin
  Service := TXXXService.Create(Configuracoes); { Lembrando que Configuracoes é uma função que criamos e que cuida do acesso ao nosso TConfiguracoes como singleton }
  Service.FazAlgo;
end;

E em testes unitários, basta criar uma implementação falsa de IConfiguracoes e passar como parâmetro no construtor de TXXXService.

Assim resolvemos o problema de testes de classes que dependem de singletons sem que precisemos deixar de utilizar singletons! Contudo, acho prudente fazer uma recomendação: Pense se você realmente precisa de um singleton. É uma [pequena] complexidade extra que você está inserindo no seu código, antes de fazer isso pense se realmente te trará alguma vantagem!

Agora, minha resposta à perguna-título do post? Eu diria que é um bom pattern, mas que requer muito cuidado de nossa parte para que não trata efeitos colaterais indesejaveis.

3

Testes unitários

March 6, 2010

Testes unitários são uma categoria de testes de software onde o sistema é dividido em unidades, que são testadas individualmente.

O que seriam essas unidades? Normalmente são os métodos das nossas classes.

Durante os testes, cada unidade é exercitada de uma forma pré-determinada e então seus resultados são comparados com os resultados esperados. Se os resultados esperados forem iguais aos resultados reais, o teste passou. Se forem diferentes, o teste não passou. É um conceito muito simples, mas incrivelmente poderoso!

Existem algumas caracteristicas que devemos buscar alcançar em nossos testes unitarios:

  1. Eles devem funcionar sem intervenção manual. Não é bom se nossos testes necessitam de informações dadas pelo usuário, ou se dependem de alguem para avaliar seus resultados. O problema é que normalmente operações manuais são mais demoradas e propensas a erro. Além disso, se os testes rodam de forma automatica, podemos por exemplo montar um ambiente de integração contínua, que é muito interessante.
  2. Eles devem funcionar de forma independente uns dos outros. Se um teste depende de outro para funcionar, um dia ele pode falhar por que você esqueceu de rodar algum teste necessario e você ficará horas debugando seu código fonte achando que há um bug no seu sistema. Para alcançar esse efeito, é importante que cada teste “limpe a casa” após o trabalho, ou seja, liberar recursos alocados, etc. Em outras palavras: Um teste nunca deve deixar rastros. Para conseguir isso, as vezes é necessario um pouco de cuidado no design do nosso código: Evite o uso de singletons, variáveis globais, variáveis de classe, etc. E use injeção de dependencias.
  3. Testes devem ser simples de executar. Deve ser algo que possa ser feito com apenas um ou dois comandos simples. Se a tarefa de executar os testes for complicada, os programadores ficarão com preguiça de executa-los e aí nosso código fica desprotegido.
  4. Testes unitários devem ser simples de escrever e de entender. Se você tem muito trabalho para escrever um teste, pode ser que haja algum problema no design do seu código. O ideal é escrever o código de produção e de testes paralelamente. Dessa forma, você garante que o seu código é simples de usar e testar, pois se você identificar alguma complicação poderá corrigir imediatamente. Um redesign no código seria muito mais trabalhoso se os testes fossem escritos apenas no final do processo.
  5. Os testes devem ser determinísticos. Isto é: Toda vez que os testes forem executados, o resultado deve ser exatamente o mesmo. Para alcançar isso, devemos organizar nosso código de forma a ficar desacoplado de recursos externos.

Ao encontrar um bug no seu código, antes de corrigir primeiro você deve criar um teste que exponha esse bug. Tendo um teste para o bug, você garante que o mesmo não será reintroduzido no futuro. Escrever o teste antes garante que o teste que você escreveu está de fato exercitando aquele bug, pois escrevendo o teste após a correção, não há garantias de que você está de fato testando a condição que estava gerando o problema.

Ao escrever os testes, procure exercitar as situações mais problematicas. Não se restrinja aos casos mais simples. Exemlo: Se você está testando uma classe que processa uma lista de dados e retorna o menor e o maior elemento, é extremamente interessante que você teste os seguintes cenários:

  1. Lista vazia
  2. Lista com apenas um elemento

Não perca tempo testando código onde é improvavel que ocorra uma falha. Por exemplo, setters e getters de propriedades, que não fazem nada além de ler e gravar uma variável. Testar esse tipo de coisa tornará a atividade de escrever testes tediosa, e os programadores se sentirão desestimulados a escreve-los. Se (note o negrito no “Se”) um dia surgir um bug relacionado a estes trechos de código não testados, aí sim você escreve um teste para capturar o bug. Mas, pense comigo, qual a probabilidade de o sistema ter um bug por causa de um método cuja única função é pegar um parametro e copia-lo para uma variável de instância? É tão baixa que nem compensa o tempo gasto para escrever os testes.

Eu já vi casos de empresas que configuraram um ambiente de integração contínua ajustado para falhar o build caso a cobertura de código (code coverage – a quantidade de código exercitada pelos testes. Existem ferramentas para fazer essa medição) não atinja um determinado percentual. Isso é, na minha opinião, algo sem sentido. Uma decisão sem muito fundamento. Tal prática só iria encorajar os desenvolvedores a criarem testes inúteis para trechos de código que não necessitam de testes (conforme expliquei no parágrafo anterior).

Como eu disse antes, cada unidade deve ser testada individualmente, e de forma determinística.  Imaginemos a seguinte situação:
Você precisa desenvolver uma classe que faça a leitura da temperatura ambiente e dispare um alarme caso a temperatura esteja maior que um certo limite. Para isso, existem dois equipamentos que devem ficar acoplados ao computador e são controlados por você: Um termometro e uma sirene.
Sua classe poderia ser mais ou menos assim:

class VerificadorDeTemperatura {
    private float getTemperaturaAmbiente() {
        /* Este método se comunica DIRETAMENTE com o dispositivo termometro e obtem a temperatura atual */
    }

    private void soarSirene() {
        /* Este método se comunica DIRETAMENE com o dispositivo sirene e faz ela disparar */
    }

    public void verificarTemperatura(float limite) {
        if (getTemperaturaAmbiente() > limite)
            soarSirene();
    }
}

Você consegue escrever os testes para essa classe seguindo as recomendações que eu listei acima? Não será possível executar os testes de forma determinística pois não temos controle sobre a temperatura retornada pelo termometro. Tambem não será possivel testar sem intervenção humana pois é necessário alguem para verificar se a sirene soou ou não a cada teste.
Devemos desacoplar a classe VerificadorDeTemperatura e os dispositivos físicos. Devemos criar interfaces para abstrair termometro e sirene. Desta forma, podemos usar implementações falsas destas interfaces nos testes, implementações estas que estão sob nosso inteiro controle. São os chamados test-doubles. O código ficaria assim:

interface Termometro {
    float getTemperaturaAmbiente();
}

interface Sirene {
    void soarSirene();
}

/* Omitirei as implementações destas duas interfaces, mas considere que são implementadas pelas classes TermometroImpl e SireneImpl, que fazem a comunicação com os dispositivos físicos */

class VerificadorDeTemperatura {
    private Sirene sirene = new SireneImpl();
    private Termometro termometro = new TermometroImpl();

    public void verificarTemperatura(float limite) {
        if (termometro.getTemperaturaAmbiente() > limite)
            sirene.soarSirene();
    }
}

Melhor assim? Não! Estamos no caminho certo, mas perceba que nossa classe VerificadorDeTemperatura ainda está fortemente acoplada aos dispositivos físicos. Ainda não temos como controla-los durante os testes pois a própria classe obtem as instancias das interfaces Sirene e Termometro.
Devemos aplicar injeção de dependencias, ou seja, a classe VerificadorDeTemperatura não mais busca as instancias de Sirene e Termometro, ao invés disso, as instancias são fornecidas a ela. O código ficaria assim:

interface Termometro {
    float getTemperaturaAmbiente();
}

interface Sirene {
    void soarSirene();
}

class VerificadorDeTemperatura {
    private Sirene sirene;
    private Termometro termometro;

    public VerificadorDeTemperatura(Sirene sirene, Termometro termometro) {
        this.sirene = sirene;
        this.termometro = termometro;
    }

    public void verificarTemperatura(float limite) {
        if (termometro.getTemperaturaAmbiente() > limite)
            sirene.soarSirene();
    }
}

Agora está bem melhor. Durante os testes, podemos ter algo como:

class SireneFalsa implements Sirene {
    private boolean soouSirene;

    public boolean getSoouSirene() {
        return soouSirene;
    }

    public void soarSirene() {
        soouSirene = true;
    }
}

class TermometroFalso implements Termometro {
    private float temperaturaAmbiente;

    public void setTemperaturaAmbiente(float temperaturaAmbiente) {
        this.temperaturaAmbiente = temperaturaAmbiente;
    }

    public float getTemperaturaAmbiente() {
        return temperaturaAmbiente;
    }

}

/* Esta é a classe que executará os testes */
public class TestVerificadorDeTemperatura {
    public void testVerificarTemperatura() {
        SireneFalsa sirene = new SireneFalsa();
        TermometroFalso termometro = new TermometroFalso();

        VerificadorDeTemperatura verificador = new VerificadorDeTemperatura(sirene, termometro);

        termometro.setTemperaturaAmbiente(50);
        verificador.verificarTemperatura(40);
        if (!sirene.getSoouSirene())
            System.out.println("Teste falhou! A sirene deveria soar");
    }
}

Claro, isto não é um teste completo. Falta testar mais condições, e falta utilizar algum framework de testes (como o JUnit) de forma a poder automatizar os testes, mas eu só queria mostrar a idéia referente ao design propício à testabilidade.

Agora imagine se você primeiro escreve toda a classe e só depois vai testar. Talvez você teria criado algo como a primeira versão que eu mostrei aqui, e depois descobriria que é impossível de testar esta classe satisfatoriamente, e então terá o maior trabalho para refazer certas coisas e ter algo testavel. Se fossse uma classe muito complexa, talvez você acabe fazendo alguma gambiarra para testar sem ter que mexer muito na classe, o que no futuro poderia acabar se tornando um problema. Ou pior: Diante de tanto trabalho que teria para corrigir a classe, alegaria que está sem tempo e deixaria sem testes mesmo.

2

Inversão de Controle (IoC) e Injeção de Dependências (DI)

February 26, 2010

Inversão de Controle (Do inglês IoC – Inversion of Control) e Injeção de Dependências (Do inglês DI – Dependency Injection) são dois assuntos de suma importancia para todo programador que pretenda escrever código reutilizavel, com boa testabilidade e de facil manutenção.

Como estes dois conceitos são frequentemente considerados (erroneamente, que fique bem claro!) sinônimos, decidi trata-los em um único post.

Primeiramente, devemos entender o que é IoC. A Inversão de Controle ocorre sempre que o fluxo de ação de um código sai do nosso controle.

Para desenvolvedores Delphi, é muito facil dar um exemplo de IoC. Vamos la: Inicie um novo projeto VCL Forms. Clique duas vezes sobre o formulário que é criado e escreva qualquer código no evento OnCreate. Pronto, isso é inversão de controle.

No mundo Java, podemos dar um exemplo igualmente trivial. Adicione qualquer listener a qualquer componente Swing, e já temos IoC.

Percebam que nos dois exemplos, o controle sobre a execução dos tratadores de evento não está em nossas mãos, mas sim nas mãos do framework (VCL no caso do Delphi, e Swing no caso do Java). O framework é que decide quando executar o seu código, e não o contrário.

Isso tambem é conhecido como princípio de hollywood: Não ligue para nós, nós ligaremos para você.

De fato, o uso de IoC é o que faz com que uma peça de software seja classificada como um framework e não como uma biblioteca. Um framework implementa a base para uma infra-estrutura da sua aplicação, cabendo a você (desenvolvedor) preencher os buracos deixados pelo framework. No caso dos exemplos dados, a infraestrutura implementada é a de interface gráfica com o usuário, e os buracos que você preenche são as respostas às ações do usuário.

Dito isto, cabe mais uma observação: Não existe framework de IoC. Inversão de controle é uma característica comum a todo framework, como eu acabei de explicar. Parafraseando Martin Fowler, dizer que um framework é especial por que implementa IoC é como dizer que meu carro é especial por que possui rodas.

Agora podemos falar sobre DI. Injeção de dependências é uma forma (mas não sinônimo!) de IoC. Posteriormente explicarei por que.

Trabalhando com DI, nossos objetos passam a não buscar suas dependencias. Ao invés disso, elas é que são dadas aos objetos.

Vamos ver um exemplo em Delphi:

type
  TDependencia = class
  end;

  TCliente = class
  private
    FDependencia: TDependencia;
  public
    constructor Create;
  end;

implementation

{ TCliente }

constructor TCliente.Create;
begin
  FDependencia := TDependencia.Create;
end;

end.

Vejam que o próprio TCliente criou uma instancia de TDependencia. Isso não é DI. Agora vamos ver o mesmo exemplo adaptado para usar injeção de dependencia:

type
  TDependencia = class
  end;

  TCliente = class
  private
    FDependencia: TDependencia;
  public
    constructor Create(Dependencia: TDependencia);
  end;

implementation

{ TCliente }

constructor TCliente.Create(Dependencia: TDependencia);
begin
  FDependencia := Dependencia;
end;

end.

Viram a diferença? Agora TCliente não mais é responsavel por criar uma instancia de TDependencia. Ao invés disso, ele já recebe uma instancia pronta. Isso é DI.

Qual a vantagem? No primeiro exemplo, ficou impossível utilizar uma subclasse de TDependencia em TCliente. Com isso, podemos fazer as seguintes afirmações:

  • A reutilização de TCliente ficou pior, pois não podemos utiliza-lo com um TDependencia diferente
  • A testabilidade de TCliente ficou pior, pois é impossivel substituir TDependencia por um test-double durante testes unitarios. Agora imagine se TDependencia é um objeto que acessa um banco de dados ou um equipamento externo? Será impossivel (ou no mínimo bem mais dificil) testar TCliente de maneira deterministica

Cabe aqui desfazer um outro mal entendido: O uso de um framework não é pre-condição para utilizar DI. Utilizando o exemplo anterior, eu poderia escrever um procedimento assim:

procedure TesteDI;
var
  Dependencia: TDependencia;
  Cliente: TCliente;
begin
  Dependencia := Nil;
  Cliente := Nil;
  try
    Dependencia := TDependencia.Create;
    Cliente := TCliente.Create(Dependencia);
    // faz algo
  finally
    Cliente.Free;
    Dependencia.Free;
  end;
end;

Perceberam a injeção de dependencias acontecendo? Espero que sim.
Perceberam o uso de algum framework? É, eu tambem não.

Contudo, existem frameworks que facilitam bastante as coisas. Neste link podemos encontrar uma lista de frameworks de DI (um deles criado por mim) para diversas linguagens de programação.

Olhando este exemplo, dá para notar como isso resolve as desvantagens que eu apontei sobre não utilizar DI? Dica: Olhe para a linha 9.

Podemos instanciar um TDependenciaA ou TDependenciaB ao invés de TDependencia, e nosso ingenuo TCliente nunca irá notar isso. Uma dessas implementações alternativas pode ser um test-double, o que nos permite escrever testes unitários para TCliente de forma bem mais simples e confiavel.

Eu disse que DI é uma forma de IoC. Agora explico o motivo dessa afirmação: Se olharmos o nosso exemplo de DI, veremos que o controle sobre a instanciação de TDependencia saiu da classe TCliente, ou seja, ocorreu uma inversão de controle.

Então voltamos ao ponto inicial do post: Muitas pessoas confundem estes dois conceitos que, embora relacionados, são bastante distintos.

5