Mostrando postagens com marcador Refatorar. Mostrar todas as postagens
Mostrando postagens com marcador Refatorar. Mostrar todas as postagens

segunda-feira, 16 de novembro de 2009

Refatorar ou reescrever?

Uma situação que já presenciei algumas vezes é alguém dizendo: “devemos reescrever essa aplicação do zero”. Mas, realmente vale a pena? E quais as situações que levam a isso?

Nas situações que presenciei, normalmente haviam dois possíveis motivos: o código era muito ruim ou houve uma grande evolução tecnológica desde que a aplicação foi escrita.

Repare que ambos os motivos são de ordem tecnológica, não tendo nada a ver com o negócio em si.

Vamos reescrever!!!

Todo o desenvolvedor gosta de criar algo novo, dificilmente alguém gosta de manter um código existente e, principalmente, escrito por outros. Quando vemos um código ruim, sempre pensamos “eu poderia fazer muito melhor” e “é melhor jogar todo esse código fora e começar do zero”.

O mesmo ocorre quando uma tecnologia antiga é utilizada em um software.

Sempre temos algumas desculpas para reescrever:
  • A tecnologia atual é muito melhor;
  • Dessa vez vamos fazer bem feito, deixaremos o código correto;
  • Entendemos melhor o produto, não vamos cometer os mesmos erros;
  • Reescrever o produto vai ser rápido, afinal, todos os requisitos já foram levantados.

Mas, nem sempre é como esperamos

Certo, decidimos reescrever toda a aplicação. Vamos deixa o antigo código de lado. Utilizamos a antiga aplicação para olhar os requisitos e copiamos o funcionamento dela em uma aplicação novinha em folha.

Aos poucos alguns problemas vão surgindo. Alguns percebemos de imediato, outros só aparecem quando já é muito tarde:
  • Os requisitos estão embutidos no código fonte: mesmo que a aplicação tenha uma ótima documentação, não podemos confiar totalmente nela. Sempre há detalhes que só estão presentes na aplicação em si. Pequenas correções, algumas features e outros pequenos detalhes. Se olhamos apenas para a documentação, deixamos passar esses detalhes, e mesmo ao se basear no código fonte, certamente muitos detalhes passarão despercebidos;
  • O mundo não para de girar: enquanto a nova aplicação é escrita, a antiga continua sendo utilizada. Sempre será necessário incluir um novo recurso ou corrigir um bug. Nesse meio tempo, enquanto reescrevemos a aplicação, toda o desenvolvimento tem que ser duplicado, é feito na versão antiga e na nova. Além disso, evitamos adicionar qualquer recurso na versão antiga, afinal, o cliente que espere a versão nova e nela fazemos isso. Infelizmente, não é bem assim que funciona na prática;
  • Vai demorar tanto quanto a primeira versão ou ainda mais: a ilusão de que o desenvolvimento será rápido logo cai por terra. Criar uma aplicação baseando-se em outra é uma tarefa difícil, além disso, temos que manter as duas versões até que a nova esteja pronta;
  • A nova versão só poderá ser utilizada quando estiver pronta: para tirar proveito da nova aplicação precisamos esperar que ela esteja 100% concluída. Nesse meio tempo, ela não adiciona nenhum valor aos desenvolvedores e nem ao cliente. Imagine se, por algum motivo, o projeto é cancelado. Todo o código desenvolvido é perdido sem nunca ter tido nenhum valor.

Então, como fazer?

A melhor tática é refatorar a aplicação existente, fazendo ajustes de forma gradual até que todo o sistema esteja da forma que desejamos. Já escrevi a respeito de como utilizar a orientação a objetos para nos ajudar nessa refatoração (veja Parte 1, Parte 2 e Final).

Podemos alterar pequenas partes do sistema ou até mesmo jogar toda uma parte fora, porém, não o sistema inteiro.

Ao fazer ajustes graduais na aplicação evitamos toda a dor de cabeça gerada pela reescrita da aplicação inteira. É claro que normalmente é mais difícil ajustar uma aplicação do que reescrevê-la, mas no final das contas, vale a pena.

Quando reescrever?

Em algumas situações realmente vale a pena reescrever totalmente a aplicação. Uma aplicação desktop que precisa ser reescrita para web por exemplo, envolve uma mudança grande de tecnologia, nesse caso vale a pena reescreve-la totalmente.

Mesmo quando decidimos reescrever uma aplicação é recomendável ter uma estratégia de como fazer isso de forma gradual. Implementando as partes mais importantes na nova versão e deixando as duas versões viverem em conjunto enquanto essa migração é feita.

Um artigo bastante interessante escrito pelo Joel Spolsky também fala a respeito disso. Recomendo a leitura: Things You Should Never Do

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.