Google Analytics

terça-feira, 26 de julho de 2011

Mais sobre as "Regras de Ouro"

Após a divulgação do artigo "As 'Regras de Ouro' do desenvolvimento de software", me pediram para falar mais sobre cada uma, dando exemplos quando possível. Vou tentar atender ao pedido neste artigo.

Não se repita.

Esta regra não se refere apenas a não dizer a mesma coisa duas vezes. Esta regra não se refere apenas a não dizer a mesma coisa duas vezes. Ela está voltada para coisas mais profundas como não armazenar a mesma informação em mais de um lugar.

Por exemplo, imagine que você tem um conjunto de arquivos que deve ser instalado pelo instalador do seu software e, consequentemente, removido na desinstalação. Seria uma quebra desta regra se você fizesse algo do tipo:
:install
  copy a b c d e f <destDir>
:uninstall
  remove a b c d e f from <destDir>
Notaram a repetição de "a b c d e f"? Neste caso para evitar a repetição, a resposta seria usar uma variável. Em outros casos, pode ser mais complicado e exigir até mesmo um gerador de código automático.

Outro caso comum de repetição acontece quando um programador (normalmente novo na equipe, mas às vezes também um antigo e com preguiça de procurar o que já existe no código) inadvertidamente cria uma função para fazer a mesma coisa que uma que já existe e que ele não conhecia.

Outro exemplo, mais sutil:

Comentários descrevendo o que uma parte de código faz. O código em si deveria ser claro o suficiente para explicar o que ele faz (leitores entenderem), além de fazer o que deve (computadores entenderem). Os comentários devem ser usados apenas para informar sobre decisões e aspectos de mais alto nível relacionados ao código. Como está muito bem dito por Andrew Hunt e David Thomas em The Pragmatic Programmer: "bad code requires lots of comments".

Outro exemplo:

Duplicação entre a especificação e o código. Ok, esta é difícil de eliminar, mas num mundo ideal seria legal ter a informação só na especificação e o código sair sozinho. Uma das ideias com UML é poder especificar usando-a e gerar código automaticamente. Bom, a gente sabe o final da história e não preciso falar a respeito. :-)

Não separe o que deve estar junto.

"O que Deus uniu, o programador não separe."

Exemplos:

Documentação de projeto (especificações, etc) devem estar guardadas junto com o código (também devido à regra de manter as coisas perto de onde são usadas - a propósito, todos sabem que as especificações devem ser usadas durante a codificação?).

cpp e h de um mesmo módulo devem estar no mesmo diretório.

Instalador de produto e código do produto devem estar na mesma árvore.

Código do módulo e unit test do módulo devem estar juntos. Aliás, eu particularmente prefiro que o código de unit test esteja dentro do mesmo arquivo que o código de produção do módulo; assim, se diminui o trabalho para criar casos de teste novos ao criar funcionalidades novas e se evita o "esquecimento" de atualizar os testes. Unit test junto do código de produção também facilita a depuração quando aparece um bug, pois facilmente se coloca o módulo suspeito em teste (lembem-se de "O procedimento de correção de bug")

Não junte o que deve estar separado ou coisas que não estão relacionadas.

"O que o programador junta, nem o diabo separa."

Exemplo clássico: O programador precisa fazer uma função que retorna nome e sobrenome, então ele concatena os dois dados numa string e retorna a string, na ilusão de que, se alguém precisar do nome e sobrenome separados (ou em um formato de concatenação diferente...), é só "parsear a string". Não caia nesta ilusão; o correto é retornar os dados separadamente, porque isto é uma interface mais genérica. Ou, o que ainda é melhor no caso de linguagem orientada a objetos: Não ter uma função que retorna o nome e o sobrenome e sim métodos que processam o nome e sobrenome internamente e resolvem todo o problema dentro da classe (exemplo: um serializador para stream).

Guarde as coisas perto de onde elas são usadas.

Aqui a questão é guardar as coisas fisicamente próximas de onde elas são usadas.

Por exemplo, eu tive um cliente que seguidamente me contactava perguntando a data em que alguma versão do software tinha sido liberada e o que tinha sido incluído naquela versão. Para dar uma resposta rápida e exata, eu precisava ter estas informações à mão perto de minha estação de trabalho. Solução: Imprimir um resumo de cada release contendo data, novidades e versão liberada e deixar estas folhas presas na parede na frente da minha mesa - guardadas onde elas seriam usadas.

Outros exemplos:

Se há uma instrução de como desligar o ar condicionado, imprima-a e coloque-a perto do aparelho ao invés de deixá-la na rede da empresa. O mesmo vale para instruções de como fechar porta, fazer café, normas de codificação, etc..

Se há um esqueleto padrão de arquivo fonte, ponha-o bem pertinho de onde os arquivos fonte vão ficar. Junto com os fontes, no sistema de versionamento, num diretório chamado skels, por exemplo.

Guarde todo o material necessário para o seu projeto de software no mesmo sistema de versionamento e faça com que seja fácil (com um comando) baixar tudo quando necessário.

Teste tudo que você faz. 
Teste cedo, teste frequentemente. Se tem 3 linhas, já devia ter sido testado. Se tem um if, já devia ter passado por 2 testes.

O que você testa, funciona de primeira; o que você não testa, dá pau. Precisa dizer mais?

6 comentários:

  1. Suponho que o primeiro paragrafo da primeira regra foi repetido de propósito. No mais, otimas instruções.

    ResponderExcluir
  2. Eu tenho o péssimo hábito de me repetir. Eu tenho o péssimo hábito de me repetir.

    ResponderExcluir
  3. Uma curiosidade sobre Unit Testing em C++. Qual software você ou vocês costumam usar?

    ResponderExcluir
  4. Alô LG,

    Sobre "O que você testa, funciona de primeira; o que você não testa, dá pau. Precisa dizer mais?":

    eu adicionaria, "o que não era para funcionar, não funciona [e de forma segura]?". Talvez seja apenas um caso especial de "O que você testa, funciona de primeira?", mas talvez valha a pena evidenciar. Há casos em que testar o que não é para funcionar pode revelar catástrofes latentes (e.g., SQL injections). :-)

    Mario

    ResponderExcluir
  5. Pessoal, agradeço muito os comentários; acrescentaram coisas importantes.

    Mateus, eu fiz a repetição de propósito. Mas obrigado por avisar. Aliás, peço a todos que sempre alertem quando virem erros nos posts.

    Fábio, eu particularmente não gosto de software / frameworks de unit testing. Na prática o que eles fazem é o embelezamento de relatórios e automatização de controles. Isto em si não é um problema; o problema é que as pessoas, ao usarem um destes sistemas acabam se empolgando tanto com as perfumarias que deixam de lado o que realmente interessa, que são os casos de teste a serem programados. Prefiro sempre começar com UT ad hoc (em C++, seria um main dentro do próprio módulo, protegido por #ifdef UNIT_TESTS, com cada caso de teste codificado com assert()). Tendo isto funcionando bem e com boa cobertura de casos de testes, se for necessário um framework, eu sugeriria cpp-unit.

    Mario, de fato sempre precisamos testar o que não era para funcionar e verificar se falha de modo seguro. E este ponto é muitas vezes esquecido. Aliás uma dica relacionada com isto: Jamais fazer catch de exception a menos que se tenha certeza de que o tratamento deixará o programa em um estado seguro. É melhor deixar o programa morrer do que deixá-lo em estado desconhecido / errante. "Dead programs tell no lies." (The Pragmatic Programmer, Andrew Hunt e David Thomas)

    ResponderExcluir
  6. Bem, eu concordo contigo mas me parece um pouco incompleta a tua resposta. Eu particularmente nunca usei Unit testing em C++. Eu tenho usado rspec que é um framework de BDD para Ruby com Rails e é esta minha exposição principal à metodologia.

    Eu não gosto muito de misturar os testes com o código testado. Acho melhor misturar os testes com outros testes. Pra ser mais claro, imagino algo como um diretório test dentro do src/ onde residam todos os testes ou talvez uma nomenclatura _test.cpp. Anyway o objetivo principal me parece ser além de testar, documentar o código. Ou seja, escrever o teste das interfaces antes de elas existirem já definindo como elas devem se comportar, fazendo os asserts necessários para os variados parâmetros de entrada e saída.

    Em rspec no Rails, por exemplo, os testes ficam em spec/ (MVC) e tu escreve o teste primeiro, faz falhar e depois implementa pra passar (não precisa necessariamente ser a mesma pessoa).

    Fazer somente um #ifdef me parece simplista e aquém do que é possível atingir. Mas evidentemente melhor do que não testar! Fiquei interessado pelo assunto agora e estou analizando algumas alternativas. Quando tiver mais assunto escrevo um novo comentário aqui (mas não espere pra logo!).

    By the way, bom trabalho no blog cara!

    ResponderExcluir