Artigo original: How (and why) to embed domain concepts in code

Imagine que o código é como uma receita de bolo: ele precisa ser fácil de entender para que qualquer um possa seguir os passos e ter o resultado esperado. Para isso, é fundamental que ele reflita claramente o problema que está resolvendo, como se estivesse falando a língua do problema.

Incorporar os conceitos do problema no código exige atenção e habilidade. Isso não acontece automaticamente só porque você está usando TDD (Desenvolvimento Orientado a Testes). Acredite, porém, que esse esforço extra vale a pena, pois resulta em um código muito mais fácil de entender e manter.

Para ilustrar, vou usar um exemplo que vivenciei em um encontro de programadores. O desafio era criar um Relógio de Berlim simplificado, que mostra as horas através de luzes que piscam em um padrão específico (veja a imagem abaixo). No nosso caso, a saída era em formato de texto, mas a ideia é a mesma.

berlin-clock-2

Solução Inicial com TDD

Muitas duplas usaram a técnica de TDD "de dentro para fora" e chegaram a soluções parecidas com esta (o código completo está no GitHub).

def berlin_clock_time(julian_time):
    hours, minutes, seconds = list(map(int, julian_time.split(":")))

    return [
        seconds_row_lights(seconds % 2)
        , five_hours_row_lights(hours)
        , single_hours_row_lights(hours % 5)
        , five_minutes_row_lights(minutes)
        , single_minutes_row_lights(minutes % 5)
    ]

def five_hours_row_lights(hours):
    lights_on = hours // 5
    lights_in_row = 4
    return lights_for_row("R", lights_on, lights_in_row)

# ...

Essa solução surge naturalmente ao aplicar o TDD, mas observe que alguns detalhes importantes sobre como o relógio funciona ficam escondidos nas funções auxiliares, como a get_five_hours.

Elevando os conceitos

Para melhorar a clareza, podemos trazer alguns desses detalhes para o topo do código, mesmo que isso quebre alguns testes (o código completo está no GitHub).

def berlin_clock_time(julian_time):
    hours, minutes, seconds = list(map(int, julian_time.split(":")))

    single_seconds = seconds_row_lights(seconds % 2)
    five_hours = row_lights(
        light_colour="R",
        lights_on=hours // 5,
        lights_in_row=4)
    single_hours = row_lights(
        light_colour="R",
        lights_on=hours % 5,
        lights_in_row=4)
    five_minutes = row_lights(
        light_colour="Y",
        lights_on=minutes // 5,
        lights_in_row=11)
    single_minutes = row_lights(
        light_colour="Y",
        lights_on=minutes % 5,
        lights_in_row=4)

    return [
        single_seconds,
        five_hours,
        single_hours,
        five_minutes,
        single_minutes
    ]

# ...

Agora, fica mais evidente que:

  • Existem 5 linhas no relógio.
  • A linha dos segundos é um caso especial.
  • Há 2 linhas para as horas e 2 para os minutos.
  • As linhas usam cores diferentes (vermelho e amarelo).
  • Cada linha tem um número diferente de luzes.

Nomeando conceitos implícitos

Apesar da melhoria, ainda não está claro como as linhas se relacionam entre si e o que cada luz representa em termos de tempo. Para resolver isso, precisamos tornar esses conceitos implícitos explícitos, dando nomes a eles.

Por exemplo, podemos passar um valor time_per_light para a função row_lights. Isso nos leva a perceber que existem dois tipos de linhas:

  • Linhas que representam a divisão inteira do tempo (//).
  • Linhas que representam o resto da divisão (%).

Ao analisarmos o primeiro caso, notamos que o segundo parâmetro é sempre 5 (5 horas ou 5 minutos). Já no segundo caso, o time_per_light (tempo por luz) é sempre 1 (1 hora ou 1 minuto), complementando o primeiro caso.

Com essa percepção, podemos reescrever as linhas da seguinte maneira:

five_hour_row = row_lights(
    time_per_light=5,
    value=hours, 
    light_colour="R",
    lights_in_row=4)

Essa mudança deixa claro que existe uma relação de pai e filho entre as linhas, onde a linha do resto depende da linha da divisão inteira.

Com base nisso, podemos criar uma função específica para as linhas "filhas" e o código fica assim (o código completo está no GitHub):

def berlin_clock_time(julian_time):
    hours, minutes, seconds = list(map(int, julian_time.split(":")))

    return [
        seconds_row_lights(
            seconds % 2),
        parent_row_lights(
            time_per_light=5,
            value=hours, 
            light_colour="R",
            lights_in_row=4),
        child_remainder_row_lights(
            parent_time_per_light=5,
            value=hours,
            light_colour="R"),
        parent_row_lights(
            time_per_light=5,
            value=minutes, 
            light_colour="Y",
            lights_in_row=11),
        child_remainder_row_lights(
            parent_time_per_light=5,
            light_colour="Y",
            value=minutes)
    ]

# ...

Agora, com uma rápida olhada no código, podemos entender quase todos os conceitos do domínio do problema:

  • A primeira linha representa os segundos e é um caso especial.
  • Na segunda linha, cada luz "R" representa 5 horas.
  • A terceira linha mostra o resto da divisão das horas por 5.
  • Na quarta linha, cada luz "Y" representa 5 minutos.
  • A quinta linha mostra o resto da divisão dos minutos por 5.

Conclusões

Incorporar os conceitos de domínio no código exige tempo e esforço – e o TDD não faz isso para você, necessariamente. No entanto, esses conceitos tornam o código muito mais fácil de entender e manter, o que economiza tempo e dinheiro a longo prazo. Lembre-se de que, como em uma receita de bolo, um código claro e organizado é essencial para que todos possam "colaborar na cozinha" sem dificuldades.