quarta-feira, 30 de setembro de 2009

Como refatorar um sistema para melhor aproveitar recursos de POO – Final

Nos dois últimos posts mostrei o código original do programa, e os passos para refatorá-lo. Leia a Parte 1 e Parte 2 se você ainda não leu. Para finalizar, quero mostrar como podemos extendê-lo de forma simples.

Imagine que agora, ao invés de ler do console, queremos ler as notas de um arquivo e salvar a média em outro. Para isso, não vamos alterar o código existente, vamos apenas criar duas classes. Uma implementando a interface ILeitorNotas e outra a interface IMostradorMedia.

Veja o código abaixo:
public class LeitorNotasEmArquivo : ILeitorNotas
{
  private string filename;

  public LeitorNotasEmArquivo(string filename)
  {
    this.filename = filename;
  }

  public List<decimal> LeNotas()
  {
    string[] valoresNoArquivo = File.ReadAllLines(filename);

    List<decimal> notas = new List<decimal>();
    foreach (string valor in valoresNoArquivo)
    {
      decimal nota = decimal.Parse(valor);
      notas.Add(nota);
    }

    return notas;
  }
}

public class MostradorMediaEmArquivo : IMostradorMedia
{
  private string filename;

  public MostradorMediaEmArquivo(string filename)
  {
    this.filename = filename;
  }

  public void ExibeMedia(decimal media)
  {
    File.WriteAllText(filename, media.ToString());
  }
}

A chamada do ProcessaMedia ficaria assim:
static void Main(string[] args)
{
  ProcessaMedia processaMedia = new ProcessaMedia(
    new LeitorNotasEmArquivo("D:\\notas.txt"), 
    new MostradorMediaEmArquivo("D:\\media.txt")
  );
  processaMedia.Executa();

  Console.ReadKey();
}

Apenas lembrando que esse código é apenas para fins de exemplo, ele não tem nenhum tratamento adicional para deixá-lo simples.

terça-feira, 29 de setembro de 2009

Como refatorar um sistema para melhor aproveitar recursos de POO – Parte 2

Na primeira parte mostrei o código original da aplicação e os seus problemas, agora vamos botar a mão na massa. Vamos refatorar esse programa, passo-a-passo, identificando os pontos onde podemos melhorá-lo.

Separar as responsabilidades

Como vimos anteriormente, nossa classe tem três responsabilidades distintas: ler as notas, calcular a média e exibir o resultado.

O primeiro passo é separar cada responsabilidade em uma classe distinta. Vamos apenas recortar o trecho de código correspondente e colocá-lo em uma nova classe.

Começamos separando a leitura de notas:
public class LeitorNotas
{
  public List<decimal> LeNotas()
  {
    List<decimal> notas = new List<decimal>();
    bool continuaLeitura = true;
    while (continuaLeitura)
    {
      Console.Write("Nota (Digite 'S' para sair): ");
      string valorDigitado = Console.ReadLine();
      if (valorDigitado.Equals("S", StringComparison.CurrentCultureIgnoreCase))
      {
        continuaLeitura = false;
      }
      else
      {
        decimal nota = decimal.Parse(valorDigitado);
        notas.Add(nota);
      }
    }

    return notas;
  }
}

E depois a exibição da média:
public class MostradorMedia
{
  public void ExibeMedia(decimal media)
  {
    Console.WriteLine("Média: "+ media);
  }
}

Nesse momento, a classe ProcessaMedia deve estar assim:
public class ProcessaMedia
{
  public void Executa()
  {
    LeitorNotas leitorNotas = new LeitorNotas();
    List<decimal> notas = leitorNotas.LeNotas();

    //RESTO DO CÓDIGO AQUI

    MostradorMedia mostradorMedia = new MostradorMedia();
    mostradorMedia.ExibeMedia(media);
  }
}

Dependendo de uma abstração

O próximo passo é alterar o código para depender de uma abstração e não de uma implementação.
Nesse caso, vamos criar a interface ILeituraNotas e a IMostradorMedia, que serão implementadas respectivamente pela classe LeituraNotas e MostradorMedia.

public interface ILeitorNotas
{
  List<decimal> LeNotas();
}

public class LeitorNotas : ILeitorNotas
{
  public List<decimal> LeNotas()
  {
    //RESTO DO CÓDIGO AQUI
  }
}

public interface IMostradorMedia
{
  void ExibeMedia(decimal media);
}

public class MostradorMedia : IMostradorMedia
{
  public void ExibeMedia(decimal media)
  {
    Console.WriteLine("Média: "+ media);
  }
}

A classe ProcessaMedia criava o LeitorNotas e MostradorMedia, agora ela não fará mais isso. Ela irá receber no construtor um objeto que implemente a interface ILeitorNotas e um objeto que implemente IMostradorMedia.

public class ProcessaMedia
{
  private ILeitorNotas leitorNotas;
  private IMostradorMedia mostradorMedia;

  public ProcessaMedia(ILeitorNotas leitorNotas, IMostradorMedia mostradorMedia)
  {
    this.leitorNotas = leitorNotas;
    this.mostradorMedia = mostradorMedia;
  }

  public void Executa()
  {
    List<decimal> notas = leitorNotas.LeNotas();

    //RESTO DO CÓDIGO AQUI

    mostradorMedia.ExibeMedia(media);
  }
}

Ao criar o objeto ProcessaMedia passamos para o construtor os objetos LeitorNotas e MostradorMedia.

Também poderíamos utilizar um framework de injeção de dependência, onde ele seria responsável por criar esses objetos auxiliares. Em um futuro post entro em mais detalhes a respeito disso.

Extendendo a funcionalidade

À partir desse momento o processamento da média não depende mais diretamente da leitura no console. Se necessário, podemos criar uma classe que implemente ILeitorNotas lendo os dados de um arquivo, por exemplo.

Um dos princípios da orientação a objetos diz que “uma classe deve estar aberta para extensão e fechada para modificação”. No exemplo, a classe ProcessaMedia está “aberta para extensões” (podemos extendê-la por meio das interfaces criadas) e “fechada para modificações” (a única razão dela ser alterada é se a regra de negócio for alterada).

Finalizando

Obviamente, esse exemplo é bastante simples e temos a sensação de “muita complexidade para pouca necessidade”. Mas repare bem, o que aumentou foi o número de classes, a complexidade foi reduzida. Cada classe tem sua responsabilidade bem definida e clara.

Espero que eu tenha conseguido mostrar o objetivo dessa refatoração. Como você pode ver, é bastante simples e provê várias vantagens.

Como refatorar um sistema para melhor aproveitar recursos de POO – Parte 1

Atualmente a programação orientada a objetos (POO) é o paradigma predominante no desenvolvimento de sistemas. Porém, apesar de todos utilizarem e saberem como funciona, nem sempre o código desenvolvido realmente utiliza o potencial da POO.

É bastante comum encontrarmos sistemas desenvolvidos com orientação a objetos mas que não passam de programas estruturados utilizando classes.

Tentarei mostrar aqui um exemplo simples de como refatorar um código utilizando alguns princípios da orientação a objetos.

O código original

O exemplo que utilizarei aqui é bastante simples, não quero me ater no programa em si, mas nos conceitos utilizados. Então, não leve muito em consideração o programa, veja o conceito para poder utilizá-lo em qualquer outro caso.

Segue abaixo o código original do programa. Como você pode notar, ele simplesmente lê várias notas, calcula a média e a exibe.

class Program
{
  static void Main(string[] args)
  {
    ProcessaMedia processaMedia = new ProcessaMedia();
    processaMedia.Executa();

    Console.ReadKey();
  }
}


public class ProcessaMedia
{
  public void Executa()
  {
    List<decimal> notas = new List<decimal>();
    bool continuaLeitura = true;
    while (continuaLeitura)
    {
      Console.Write("Nota(Digite 'S' para sair):");
      string valorDigitado = Console.ReadLine();
      if (valorDigitado.Equals("S", StringComparison.CurrentCultureIgnoreCase))
      {
        continuaLeitura = false;
      }
      else
      {
        decimal nota = decimal.Parse(valorDigitado);
        notas.Add(nota);
      }
    }

    decimal totalNotas = 0;
    int qtdNotas = notas.Count;
    foreach (decimal nota in notas)
    {
      totalNotas += nota;
    }

    decimal media = 0;
    if (qtdNotas > 0)
    {
        media = totalNotas/qtdNotas;
    }

    Console.WriteLine("Média: "+ media);
  }
}

Quais os problemas nesse código?

Quais os problemas nesse código e o que podemos fazer para melhorá-lo? Vejamos:
  1. A classe ProcessaMedia tem três responsabilidades. Ela é responsável por lêr a nota, calcular a média e exibir o resultado. O ideal é que uma classe tenha apenas uma responsabilidade;
  2. Ela está diretamente acoplada ao Console. Isso pode gerar alguns problemas, por exemplo, como você faria um teste unitário dessa função? Simplesmente não é possível.
  3. Ela não é extensível. Se, por exemplo, surgir a necessidade de dar a opção de ler as notas de um arquivo, como você faria? No código seriam feitos uma série de condições IF?
Para resolver esses problemas vamos adotar algumas medidas:
  1. Separar cada responsabilidade em uma classe;
  2. A classe CalculaMedia deverá depender de uma abstração, e não do Console;
  3. Com a implementação dos itens 1 e 2 automaticamente ganhamos a extensibilidade, nenhuma medida adicional será necessária.

Mãos na massa

No próximo post mostrarei os passos para refatorar esse código.

Apesar deste ser um exemplo simples, podemos aplicar os mesmos conceitos em muitas outras situações. Pense um pouco e você logo vai lembrar de situações semelhantes a esse exemplo.

quinta-feira, 24 de setembro de 2009

Introdução ao TDD

A algum tempo comecei a adotar a prática de TDD (Test-Driven Development). No início pensava "não tem como desenvolver desse jeito", em pouco tempo passei a pensar "não tem como desenvolver sem isso".

Existe muita informação na Internet a respeito de TDD, por isso não quero falar de como fazer, mas sim, dos conceitos e práticas importantes para quem está começando agora. Para quem está começando, parece que TDD é relacionado apenas a "testes automatizados", mas é muito mais que isso.

Definição de TDD

A prática de TDD se resume a três passos simples, mas fundamentais:
  • Escrever um teste que falhe - o primeiro passo é pensar o que deseja-se fazer. Com isso, criar um teste definindo o contexto, a execução e as expectativas;
  • Fazer o teste passar - deve-se escrever o mínimo de código necessário para fazer o teste passar, nada além disso;
  • Refatorar - deve-se rever o código e verificar se há melhorias a serem feitas. Essas melhorias não podem alterar o comportamento apenas a estrutura do código.
Apesar dos passos serem simples, no início é difícil segui-los. Nossa tendência é escrever imediatamente todo o código e no final adicionar os testes. É justamente esse o ponto mais crítico e mais importante na adoção de TDD, se acostumar a escrever o teste primeiro. Toda a vantagem do desenvolvimento dirigido por testes vêm disso.

Podemos utilizar as três regras definidas por Robert Martin:
  • Você não pode escrever nenhum código a não ser que ele seja necessário para fazer um teste que esteja falhando passar;
  • Você não pode escrever nenhum teste além do necessário para falhar. E, falhas de compilação também são falhas;
  • Você não pode escrever nenhum código além do necessário para fazer o teste passar;
Em resumo, só escrever qualquer código necessário para atender um teste que esteja falhando, nada além disso.

Benefícios

A grande pergunta é: quais os benefícios de TDD?

Para quem ainda não conhece a prática, a única vantagem vista são os testes unitários. Ninguém vê logo de cara os demais benefícios que essa prática traz. Alguns deles que posso destacar:
  • Requisitos melhor detalhados - os testes ajudam a entender melhor os requisitos;
  • Codificar apenas o necessário - como apenas é codificado o mínimo necessário para passar um teste evitamos introduzir códigos que não temos certeza se serão utilizados;
  • Reduz o acoplamento das classes - para que os testes sejam fáceis de serem criados, as classes não podem ter um forte acoplamento;
  • Deixa claro o comportamento do sistema - para um desenvolvedor que entra no projeto, ou quando vamos alterar um código existente, sempre podemos consultar os testes para saber qual o comportamento esperado;
  • Incentiva a refatoração - é bastante comum ouvirmos a frase "é melhor não mexer ali pois não sabemos quais os problemas que pode gerar". Com um sistema com boa cobertura de testes, o desenvolvedor tem muito mais confiança para fazer qualquer alteração;
  • Menor utilização do depurador - com os testes é muito mais fácil verificar o comportamento. Evitamos ter que abrir o sistema, acessar meia dúzia de telas e só depois testar a funcionalidade;
Um fato interessante da prática de TDD é que ele mostra claramente os pontos do sistema onde o design está ruim. Basicamente, se está difícil ou trabalhoso testar determinada parte do sistema, isso é um grande sinal de um problema no design.

O que testar?

O ideal é que 100% do sistema esteja coberto por testes, porém isso dificilmente é possível. Há determinados pontos do sistema onde não é possível criar testes unitários ou o trabalho não compensa.

Normalmente o código que acessa recursos externos (banco de dados, sistema de arquivos, etc) não pode ser testado com testes unitários. Para estes casos é necessário criar testes integrados que, normalmente são mais difíceis de serem criados/mantidos e mais lentos. Deve-se tentar desacoplar ao máximo o sistema destes recursos externos.

Vale a pena?

No início é difícil se acostumar a essa nova forma de trabalho, porém em pouco tempo percebemos os benefícios e não queremos voltar atrás. No momento que percebi que estava gastando muito menos tempo depurando os programas e o código estava melhor, vi que esse era um caminho sem volta.

Primeiro post

Finalmente decidi criar um blog. Após algum tempo pensando em criar um, resolvi tentar e ver como será.

Criei esse blog com alguns objetivos:
  • Melhorar o meu conhecimento nas áreas que atuo - sempre que preciso explicar algo acabo aprendendo mais sobre o assunto;
  • Passar o conhecimento adiante - gosto de compartilhar tudo que aprendo. Ao invés de compartilhar esse conhecimento por e-mail ou de alguma forma fechada, um blog vai permitir tornar isso público. Quem sabe mais pessoas podem se aproveitar disso;
  • Aprender a redigir melhor - algumas vezes colocar em palavras os pensamentos é um tanto complicado, esse blog pode me ajudar bastante;
  • Ter um banco de lições aprendidas - muito do que aprendi hoje vou precisar amanhã e provavelmente já terei esquecido. Com um blog, posso deixar isso salvo para minha própria pesquisa futura;

Como desenvolvo principalmente em .Net, mais especificamente com a linguagem C#, o conteúdo do blog provavelmente será voltado a essa área. Porém, não quero me ater a parte técnica, pretendo postar a respeito de práticas para melhorar a qualidade do software como um todo.

Atualmente estou procurando melhorar a qualidade dos softwares que desenvolvo e ter uma maior efetividade no desenvolvimento. Para isso, estou adotando práticas do desenvolvimento ágil, e aperfeiçoando meu entendimento de programação orientada a objetos. Essa é uma área muito interessante e que pretendo escrever a respeito.

Então, é isso. Vamos ver como será daqui por diante. Certamente ganharei muita experiência e espero poder contribuir um pouco com os outros também.

Caso você tenha algum comentário, sugestão ou crítica, por favor deixe um comentário. Toda opinião é bem vinda.