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.

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.