Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Deixe o computador garantir isso por você

By Joshua FoxNov 30, 20218 min read

Esta página também está disponível em English, Deutsch, Español, Français, Italiano e 日本語.

Quem está começando a programar ouve o tempo todo que precisa comentar o código o máximo possível. Mas não demora muito até você se deparar com um artigo dizendo o contrário. Confuso? Este artigo vai te dar um panorama claro de quando comentar e quando deixar pra lá.

O princípio é simples: deixe a linguagem e o runtime garantirem o quanto antes que aquilo que você documenta sobre o software seja verdade.

Nossos fiscais robôs

Vou começar com uma função sem documentação alguma e ir evoluindo na ordem em que costumo implementar as coisas. Primeiro, o básico fácil; depois, a verificação automatizada de que a documentação diz a verdade; e, por fim, as formas mais fracas de documentação para quando a verificação automatizada não estiver disponível.

1\. Sem documentação

Vamos começar com esta função simples, p!, fatorial, tirada direto dos exercícios básicos de programação. O exemplo está em Python, mas vale basicamente para outras linguagens também.

def fact(p):
    ret = 1
    if x == 0:
        return 1
    for i in range(1, x + 1):
        ret *= i
    return ret

Você pode ficar tentado a adicionar um comentário do tipo:

""" This is the factorial """

Mas comentários muitas vezes não são lidos, e esse fica redundante quando a nomenclatura está bem feita.

2\. Nomenclatura

Mude a função para:

def factorial(p):...

A documentação agora está embutida no código que é executado, e não em uma camada paralela. As pessoas precisam ler esse nome se quiserem usar sua função. Esse é um bom motivo para que funções sejam curtas e tenham um único propósito: assim, o nome da função fica próximo do código e é descritivo dele.

Quaisquer outras melhorias que ajudem o leitor a entender o código (por exemplo, usar membros privados quando fizer sentido) são uma forma decente de documentação.

Em seguida, renomeie o parâmetro para n, para lembrar quem usa que se trata de um inteiro.

def factorial(n):...

Afinal, embora o fatorial seja definido apenas para inteiros, ele pode ser estendido a não inteiros. A função Gama é uma espécie de fatorial estendido.

Um usuário avançado pode se perguntar se é isso que a nossa implementação faz, e com razão: scipy.special.factorial, por exemplo, de fato retorna um valor para entradas float não inteiras, usando a interpolação Gama do fatorial. Precisamos deixar claro que não é isso que estamos oferecendo aqui.

A função Gama estende o fatorial para quase todos os números reais (e complexos).

3\. Type Hints

O nome do parâmetro é uma espécie de comentário, e você poderia escrever um comentário dizendo que ele é um int (como faz o scipy.special.factorial). Mas existe uma forma melhor de avisar ao leitor que o parâmetro é um inteiro: os type hints.

def factorial3(n: int) -> int:...

O tooling de desenvolvimento do Python vai te avisar sobre a maioria dos erros. O princípio: deixe o computador documentar por você e reduza ao mínimo a dependência do cérebro humano. Por exemplo, se sua implementação não aceitar o float 3.0 como se fosse um int, o type hint vai documentar isso na hora — algo que poderia passar batido de outra forma.

Comentários podem ficar desatualizados ou simplesmente errados. Por exemplo, o scipy.special.factorial (sem implicar com o SciPy — é uma biblioteca excelente!) diz no comentário " n : int", mas, na prática, aceita float e até retorna um valor seguindo a função Gama.

Tipagem em tempo de compilação

Nossos exemplos são em Python, mas, se você usa uma linguagem com tipagem em tempo de compilação como Java ou C++, ao declarar tipos o compilador efetivamente bloqueia muitos erros já em tempo de desenvolvimento, garantindo que essa "documentação" seja precisa o quanto antes.

Ainda assim, assertions são melhores para capturar restrições mais finas, como a proibição de números negativos como entrada — a menos que você esteja usando uma linguagem com um sistema de tipos mais avançado.

4\. Assertions, exceções ou pré e pós-condições

Dá pra ir um passo além dos type hints. Faça uma assertion de que o valor é um inteiro e outra de que ele é não negativo. Assim, não é só o tooling de desenvolvimento dando uma piscadinha: o runtime vai barrar os erros assim que acontecerem.

def factorial4(n: int) -> int:
    assert isinstance(n, int), f"{n} is not an integer and so unsupported."
    assert n >= 0, f"{n} is negative and so unsupported."
    ...

Há diferentes maneiras de lançar o erro.

Como não faz sentido deixar esses erros passarem, mantenha as assertions habilitadas. Mas, alternativamente, você pode lançar um ValueError. Se estiver usando uma linguagem ou biblioteca que dê suporte a programação por contrato, com pré e pós-condições (uma espécie de assertion estruturada), isso também resolve.

@icontract.require(lambda n:  isinstance(n, int) and n>=0, "n must be a nonnegative integer")
def factorial_precondition(n: int) -> int:
    return  __iterative_factorial(n)

Nesse ponto, o computador realmente está checando o nosso trabalho. Mas as assertions ou outras exceções têm um segundo valor: são uma forma de documentação que não tem como estar errada. O leitor sabe que o que uma assertion afirma é verdade.

Voltando à nossa piñata, o scipy.special.factorial, o comentário diz " If n < 0, the return value is 0."

Tecnicamente, isso é verdade nessa implementação, mas confunde o usuário que sabe que o fatorial não é definido para números negativos. Mesmo a Gama, extensão do fatorial — embora definida para os reais e até para os complexos —, é indefinida em inteiros negativos. Muito melhor lançar um erro e deixar bem claro o que é permitido e o que não é.

5\. Testes unitários

Testes unitários se parecem com assertions: verificam que o sistema faz o que você espera durante o desenvolvimento. Como documentação, são mais fracos que as assertions, porque ficam distantes do código — tanto nos arquivos em que vivem quanto no momento em que são executados. É menos provável que outros devs os leiam.

Ainda assim, se os desenvolvedores rodam os testes continuamente, como deveriam, vão começar a ver os erros logo, sem precisar fuçar nos comentários.

Por exemplo, nosso fatorial iterativo simples está quebrado! Ele retorna o valor 1 para valores negativos, o que está claramente errado, tanto para o fatorial quanto na extensão Gama. Este teste mostra que esperamos um erro, mas, infelizmente, recebemos um valor de retorno.

def test_factorial():
    with pytest.raises(Exception):
        factorial(-2)

Você pode até colocar mini testes unitários dentro de comentários usando o doctest, um recurso nativo do Python 3. Isso deixa o teste o mais próximo possível do código, onde ele pode servir melhor como documentação (embora também possa poluir a leitura).

def factorial_doctest(n) -> int:
    """
    Run this with python -m doctest -v factorial.py

    >>> factorial_doctest(3)
    >>> factorial_doctest(0)
    >>> factorial_doctest(1.5)
    Traceback (most recent call last):
      ...
    TypeError: 'float' object cannot be interpreted as an integer
    """
    return __iterative_factorial(n)

Por consequência, testes de integração estão um passo mais distantes do código e são menos adequados para documentá-lo. Em vez disso, eles documentam o comportamento do sistema como um todo.

6\. Logging

Se coisas ruins vão acontecer e você não tem como evitá-las antes, ao menos registre em log. De volta ao SciPy.

Se você passa um float, recebe isto na saída de erro padrão:

DeprecationWarning: Using factorial() with floats is deprecated

Aparentemente eles usaram uma implementação baseada em Gama e, mais tarde, ao perceberem que apenas inteiros deveriam ser suportados, quiseram restringir a entrada a int. Como não conseguiram fazer isso por causa de aplicações dependentes, ao menos escreveram o aviso em um lugar onde ele tem chance de ser lido. Devs podem não ler a documentação, mas, quando topam com problemas, leem os logs.

7\. Comentários

Chegamos agora ao nosso penúltimo recurso: o comentário. Há lugar para eles, mas só, na minha experiência, nestes três casos:

a. APIs públicas devem ser documentadas com comentários inline, seguidos da geração de HTML com Doxygen no Python, Javadocs no Java etc. Você deve estruturar os comentários com tags, seguindo o princípio de que, quando o computador pode verificar a estrutura, é exatamente isso que ele deve fazer.

Um exemplo dos comentários estruturados em Python:

:param n: A nonnegative integer
:return: The product of all integers from 1 up to and including n; or for 0, return 1.

Mas, quando a função não é uma API pública, use esses campos estruturados com parcimônia: esses comentários servem para passar uma mensagem específica sobre algo incomum, não para documentar meticulosamente todos os aspectos de uso.

b. Fatos surpreendentes devem ser documentados. Por exemplo, os usuários podem não saber que o fatorial de zero é definido como 1, já que isso não parece se encaixar na forma como o fatorial é definido como "o produto de todos os números de 1 até n." Da mesma forma, contornos estranhos e gambiarras devem ser comentados no código.

c. Algoritmos devem ser nomeados, já que o computador não consegue mostrar nem garantir qual algoritmo está em uso, e o leitor não necessariamente o identifica de bate-pronto. Por exemplo, o fatorial pode ser calculado por multiplicação ou aproximado por vários algoritmos para a Gama. Um usuário com aplicações numéricas exigentes pode querer saber disso.

def factorial(n: int) -> int:
    """
    This is a simple iterative implementation of factorial.
    The input must be a non-negative integer.
    Note that the factorial of zero is defined as 1.
    """

8\. Documentação externa

Os comentários eram o penúltimo recurso; a documentação externa é o último de todos. RTFM é um ótimo lema, mas poucos leem o manual, já que ele fica tão distante do código que é fácil ficar desatualizado. Ainda assim, se você precisa de uma visão geral das funcionalidades ou de tutoriais, dá para colocar tudo em um documento mantido separadamente do código.

Mas atenção: você precisa revisar e reler esses documentos continuamente se quiser que eles reflitam o que está no código em constante mudança.

Quanto mais perto do código e mais automatizado, melhor — mas tudo tem sua utilidade.

A documentação deve dizer a verdade. O tooling da sua linguagem e o runtime têm muitas formas de garantir confiabilidade, então use-as. Coloque a documentação o mais próximo possível do código e faça com que a verificação automatizada da precisão da documentação aconteça cedo no processo de desenvolvimento.

O código executável das diversas implementações está aqui, com testes unitários: https://github.com/doitintl/commenting

Para ficar por dentro, siga a gente no DoiT Engineering Blog , no canal da DoiT no LinkedIn e no canal da DoiT no Twitter . Para conhecer oportunidades de carreira, acesse https://careers.doit-intl.com .