Mudanças simples x Soluções simples

O processo de entrega de uma funcionalidade nova envolve implementar ou corrigir algo que o nosso sistema atualmente não comporta. Dados os problemas que o programa resolve nesse instante, existem diversas maneiras distintas de resolvê-los, todas válidas, algumas mais limpas e mais simples do que outras.

O gráfico a seguir mostra uma situação onde nosso programa não resolve o problema atual que desejamos atacar, onde:

a) vermelho: representa variações do sistema que não resolveriam meu problema;
b) amarelo: minha situação atual;
c) verde: soluções possíveis.

Soluções possiveis para um problema

Soluções possiveis para um problema

Mas dentro das soluções possíveis, existem aquelas mudanças que são simples de serem executadas, por exemplo incluir um “if” ou simplesmente fazer um copy e paste de código, um pequeno passo, uma mudança simples, um baby step. Mudanças simples que resolvem o problema, representadas em verde escuro aqui:

Mudanças simples

Mudanças simples

Por fim, existem as soluções mais simples para o problema. A solução mais simples pode ser extrair uma classe ou utilizar uma biblioteca que executa a lógica para nós, por exemplo. No diagrama a seguir as soluções mais simples são representadas em azul:

Soluções simples

Soluções simples

A execução desses passos, baby steps, traz o código para dentro das mudanças simples que resolvem o problema. Mas a sequência dos mesmos pode nos levar para diversos lugares diferentes: alguns mais próximos das soluções mais simples, outros cada vez mais distantes das mesmas, nos deixando com um débito técnico cada vez maior.

Pior ainda, a adoção cega de baby steps ou de mudanças simples sem nenhuma prática para garantir a qualidade pode levar a uma situação onde seja impossível chegar em uma solução simples: seu sistema vira uma grande bola de lama.

Essa situação é o inferno de qualquer sistema: passos simples não são mais capazes de manter as coisas simples.

Uso puro de baby steps

Uso puro de baby steps não implica em melhoria de qualidade. Passos muito simples podem levar seu sistema a situações negativas.

Em algumas situações, o baby step pode implicar em uma das melhores soluções, mas para isso é necessário o domínio da tecnologia, do negócio, design e linguagem, algo que nem sempre é a realidade do desenvolvedor quando escreve uma funcionalidade:

Baby step e design

Baby step de um desenvolvedor com domínio forte da tecnologia, linguagem, domínio de negócio e sistema.

Como é difícil executar um passo-a-passo muito suave e chegar em uma solução simples qualquer, adicionamos mais uma técnica ao processo: além de executar as tarefas em pedaços pequenos (mas não mínimos), refatoramos nosso código, para simplificá-lo.

Cada passo de simplificação através da refatoração coloca nosso código mais próximo do sistema ideal: o mais simples de todos que resolve nossos problemas.

Refatorando

A refatoração que a cada passo simplifica o código nos leva cada vez mais próximos a melhor solução para nossos problemas.

Com a refatoração contínua, cada passo que damos ao implementar uma nova feature nos mantém ainda próximos das soluções simples.

Mais importante do que fazer passos minúsculos (baby steps) é fazer passos que fazem sentido para levá-lo em direção a melhor solução (e não somente a mudança mais simples). Para isso não podemos nos esquecer de refatorar. E não só refatorações de baixo nível (como renomear variáveis, mover métodos, etc), mas sim refatorações de alto nível (como abstrair comportamentos).

Imagine o seguinte trecho de código, responsável por calcular o salário de um funcionário. Dependendo do cargo do funcionário, o algoritmo para o cálculo é diferente.

public class CalculoDeSalario {

  public CalculoDeSalario(RegrasFiscais regrasFiscais, ...) {
    // ...
  }

  public double calcula(Funcionario f) {
    if(f.getCargo() == Cargo.VENDEDOR) {
      // calcula salario do vendedor usando regras fiscais, etc
    }

    if(f.getCargo() == Cargo.GERENTE) {
      // calcula salario do gerente usando regras fiscais, etc
    }
  }
}

Agora suponha que um novo cargo apareceu na empresa e precisamos calcular o salário das pessoas que farão o trabalho desse novo cargo. O código mais simples que faria isso acontecer, seria:

public class CalculoDeSalario {

  public CalculoDeSalario(RegrasFiscais regrasFiscais, ...) {
    // ...
  }

  public double calcula(Funcionario f) {
    if(f.getCargo() == Cargo.VENDEDOR) {
      // calcula salario do vendedor usando regras fiscais, etc
    }

    if(f.getCargo() == Cargo.GERENTE) {
      // calcula salario do gerente usando regras fiscais, etc
    }

    if(f.getCargo() == Cargo.MARKETING) {
      // calcula salario do marketing usando regras fiscais, etc
    }
  }
}

Mas essa não é a melhor solução que resolveria o problema. A cada novo if inserido nesse código, mais longe estamos da melhor solução. O design fica cada vez mais rígido. E pior: refatorar esse trecho de código fica cada vez mais difícil: até mesmo o segundo if que foi adicionado ao código já havia o afastado da melhor solução!

Ao implementar uma nova funcionalidade, o programador deve observar o design atual da sua aplicação. Se o design atual já provê uma maneira fácil de implementar a funcionalidade, então vá em frente. Caso contrário, o programador deve refatorar o design para permitir que você possa adicionar essa funcionalidade da maneira mais simples possível.

No exemplo acima, uma solução seria criar uma estratégia (do padrão de projeto Strategy) para calcular o salário de acordo com o cargo. Cada cargo seria responsável por devolver o seu algoritmo de cálculo. Por exemplo:

public class CalculoDeSalario {

  public CalculoDeSalario(RegrasFiscais regrasFiscais, ...) {
    // ...
  }

  public double calcula(Funcionario f) {
    AlgoritmoDeCalculoDeSalario calculadora = f.getCargo().getAlgoritmoDeCalculo(regrasFiscais);
    return calculadora.calcula();
  }
}

Se essa é a melhor solução, é difícil dizer. Mas com certeza ela é uma solução melhor do que as anteriores. Mas, veja que após essa refatoração, implementar o cálculo de salário para o novo cargo é muito mais fácil. Agora posso voltar aos meus baby steps, já que a implementação corrente está em um lugar seguro.

Toda essa experiência se resume a uma frase que surgiu em uma conversa entre Guilherme Silveira e Maurício Aniche:

A mudança mais simples para resolver um problema não é necessariamente a solução mais simples para resolvê-lo.

3 Comentários

  1. tucaz 18/11/2010 at 18:50 #

    Muito bom, meu querido!

    ps: o codigo nao aparece quando le-se via feed

  2. Carlos Galdino 22/11/2010 at 13:12 #

    Já acompanhava os (ótimos) posts do Aniche no seu blog pessoal e é muito bom ver agora a opinião dele aqui também.

    Só um detalhe, no último trecho de código seria interessante aplicar a Law of Demeter pra pegar o algoritmo de cálculo.

  3. Guilherme Silveira 23/11/2010 at 11:36 #

    Opa Antonio Carlos! Obrigado pelo toque do código, a tag que usamos é de adição de javascript via gist e o feed bloqueia isso.

    Opa Carlos, com certeza. Uma refatoração possível para tentar diminuir o acoplamento entre a classe atual e os métodos das interfaces com a qual ela se comunica seria aplicar Demeter e diminuir uma invocação.

    Abraço!

Deixe uma resposta