Duas cabeças pensam melhor que uma, já dizia o ditado. E muitas vezes é verdade mesmo. Por isso, a prática da programação em par, ou pair programming tem ganhado tanta força mundo afora.
Pair programming
A ideia básica é simples: tal como no rally, em que o piloto e o navegador colaboram para vencer a corrida, no pair programming a dupla colabora numa dinâmica parecida para atingir um objetivo.
Um programador assume o papel de piloto (e o teclado), e o outro de navegador, responsável por acompanhar o desenvolvimento e antecipar os próximos passos. De tempos em tempos, os dois trocam de papel.
Infelizmente, apesar do potencial enorme para trazer bons resultados, é bastante frequente presenciarmos cenas como esta:
Ou então, depois de alguns minutos codificando você olha para o lado para pedir a opinião do seu colega só para encontrá-lo distraído no celular, e ele nem sabe mais o que está acontecendo. É uma situação bastante desagradável, e pode ter vários motivos.
Pode ser que as duas pessoas não saibam trabalhar juntas, pode ser que uma delas está num dia ruim, mas um motivo muito mais frequente é a falta de disciplina de pelo menos um dos praticantes, ou a falta de conhecimento de uma forma eficaz de trabalhar em par.
E é aí que entra a metadologia conhecida como Ping-pong Pair Programming.
Ping-pong Pair Programming
O Ping-pong Pair Programming é, na minha humilde opinião, uma metodologia incrível. De todos os formatos de pareamento que já testei, essa foi a única que se mostrou infalível em manter a dupla focada durante o processo de desenvolvimento.
Ao invés de trocar os papéis de tempos em tempos, num intervalo definido, a dinâmica do ping-pong toma vantagem do processo de desenvolvimento do TDD (Test Driven Development). O ciclo de vida do TDD atua como um cronômetro, ditando o momento das trocas de papel.
Ao implementar uma nova funcionalidade usando TDD, a dupla vai naturalmente repetir as três fases do ciclo.
Para quem não conhece o TDD, vou deixar alguns materiais de referência no fim do artigo. Mas resumidamente, a prática consiste em guiar o desenvolvimento pelos testes, ou seja, primeira a gente escreve os testes, e só depois o código de produção, mas somente a quantidade necessária para fazer o teste passar.
No ciclo de vida do TDD, começamos escrevendo um teste, que naturalmente não vai passar (e no começo, nem mesmo compilar). Este teste é responsável por guiar a implementação, e nele expressamos o resultado que esperamos do código que vamos escrever em seguida. Se pararmos para refletir por um momento, não tem como escrever o teste antes sem compreender a regra de negócio que precisamos implementar. E a melhor parte, é que isso acaba com aquele impulso de sair escrevendo código sem um plano claro.
Escrito um teste que falha por aguardar a implementação, chegamos ao fim da fase vermelha (Red), e é hora de trocar de papéis. Para assumir o papel de piloto, e poder implementar o código necesssário para fazer o teste passar, o outro programador precisa estar prestando atenção, participando ativamente, ou vai ficar perdido quando chegar a sua vez. O trabalho dele é escrever o mínimo código necessário para fazer o teste passar, chegando ao fim da etapa verde (Green). É o momento de refatorar aquilo que não ficou bacana, tanto no código produtivo, quanto nos testes.
E então, chega a hora de escrever um novo teste, deixar ele quebrando, e passar a bola para o colega, e ir repetindo este ciclo.
Um exemplo prático, passo-a-passo
Nada melhor que um pouco de código para ajudar a visualizar o processo de desenvolvimento com pareamento ping-pong. A seguir veremos o comecinho do processo de implementação de uma função para calcular a sequência de Fibonacci, codificada em Python.
A primeira atividade que a dupla (Fulano e Ciclano) deve executar, é entender a regra de negócio que pretende implementar. Pesquisando na Wikipédia, eles entendem que a sequência de Fibonacci é uma sequência de números inteiros, que começa com 0 e 1, e cujo cada número subsequente é a soma dos dois anteriores.
Fulano começa (PING)
A dupla escolhe o piloto, e ele se responsibiliza por começar a implementação. Seguindo a boa prática de baby steps, ele implementa o menor teste que pode conceber:
import unittest
class FibonacciTests(unittest.TestCase):
def test_o_primeiro_valor_eh_zero(self):
valor = fibonacci(pos=0) # <-- essa função ainda nem existe!
self.assertEqual(valor, 0)
if __name__ == '__main__':
unittest.main()
Sabendo que a regra dita que o primeiro valor retornado pela sequência é zero, o piloto escreveu um simples teste, que chama a futura função fibonacci
, passando a pos
0, esperando que o valor retornado seja 0. Chegando ao fim da etapa RED
do ciclo do TDD, ele passa a bola.
Ciclano assume (PONG)
Ciclano também está ciente dos baby steps, e escreve a menor implementação possível para fazer passar os testes, e chegar à fase GREEN
do ciclo do TDD:
def fibonacci(pos):
return 0
Ele roda os testes, e está passando. Excelente.
E então chega a hora de refatorar (etapa REFACTOR
). Ele olha para Fulano, e chega à conclusão de que ainda é cedo pra isso, e parte para escrever o segundo teste:
def test_o_segundo_valor_eh_um(self):
valor = fibonacci(pos=1)
self.assertEqual(valor, 1)
Ele roda o teste, e o mesmo falha (RED
). É hora de passar a bola. O código ficou assim:
import unittest
def fibonacci(pos):
return 0
class FibonacciTests(unittest.TestCase):
def test_o_primeiro_valor_eh_zero(self):
valor = fibonacci(0)
self.assertEqual(valor, 0)
def test_o_segundo_valor_eh_um(self):
valor = fibonacci(pos=1)
self.assertEqual(valor, 1)
if __name__ == '__main__':
unittest.main()
Fulano assume (PING)
Primeiro, ele faz o mínimo para o novo teste passar (GREEN
):
def fibonacci(pos):
if pos == 1:
return 1
return 0
Essa foi rápida. Acabou o aquecimento. Os dois poderiam ficar nesse loop de adicionar um valor por vez por toda a eterninadade, mas isso não seria muito produtivo. É hora de deixar as coisas interessantes e tacar lenha na fogueira! Então Fulano escreve o próximo teste, forçando o colega a implementar uma versão do algoritmo que atenda os próximos valores da sequência (RED
):
from unittest.mock import patch
# ...
@patch('builtins.input', return_value=[(2, 1), (3, 2), (4, 3), (5, 5), (6, 8), (7, 13)])
def test_o_proximo_valor_eh_a_soma_dos_dois_anteriores(self, retornos):
for par in retornos():
posicao, valor_esperado = par
valor = fibonacci(posicao)
self.assertEqual(valor, valor_esperado)
Ciclano assume (PONG)
Agora as coisas ficaram mais divertidas, e essa rodada talvez seja um pouco mais demorada. Após refletir por uns instantes, decide implementar o algoritmo com uma abordagem procedural, e chega neste resultado:
def fibonacci(pos):
anterior = 0
proximo = 0
for _ in range(0, pos):
proximo = proximo + anterior
anterior = proximo - anterior
if(proximo == 0):
proximo = proximo + 1
return proximo
Ele roda os testes e estão passando (GREEN
). Ele decide refatorar levemente o código (REFACTOR
), renomeando a variável pos
para posicao
. O código ficou assim:
def fibonacci(posicao):
anterior = 0
proximo = 0
for _ in range(0, posicao):
proximo = proximo + anterior
anterior = proximo - anterior
if(proximo == 0):
proximo = proximo + 1
return proximo
# e os testes também:
def test_o_primeiro_valor_eh_zero(self):
valor = fibonacci(posicao=0)
self.assertEqual(valor, 0)
def test_o_segundo_valor_eh_um(self):
valor = fibonacci(posicao=1)
self.assertEqual(valor, 1)
O piloto está um pouco indeciso sobre qual teste escrever em seguida. Ele pergunta para o copiloto Fulano se tem alguma ideia, e o amigo o lembra que a sequência de Fibonacci só compreende números positivos. Eles decidem que nesses casos, a função deve retornar zero. Então Ciclano escreve este teste:
def test_deve_retornar_zero_para_posicoes_negativas(self):
valor = fibonacci(-1)
self.assertEqual(valor, 0)
Ao rodar este teste, ele passa, porque coincidentemente o for
não é processado. Então não conta como RED
e ainda não pode passar a bola para o amigo.
Olhando para o artigo na Wikipedia, ele decide que talvez não caiba escrever novos testes. Adiciona mais alguns casos no teste test_o_proximo_valor_eh_a_soma_dos_dois_anteriores
e decide desafiar o colega a refatorar a função para a versão recursiva. O colega topa. Eles continuam na etapa REFACTOR
.
Fulano assume (PING)
Ele começa a refatorar a função, e chega a esta versão, que atende a todos os testes já escritos:
def fibonacci(posicao):
if posicao <= 0:
return 0
if posicao == 1:
return 1
else:
return fibonacci(posicao - 1) + fibonacci(posicao - 2)
Os dois olham um para o outro com um aceno de que fizeram um bom trabalho. Fulano decide mover a função para um módulo fib.py
e estão prontos. Já podem ir tomar um café. :)
O arquivo fib.py
ficou assim:
def fibonacci(posicao):
if posicao <= 0:
return 0
if posicao == 1:
return 1
else:
return fibonacci(posicao - 1) + fibonacci(posicao - 2)
E o arquivo de testes ficou assim:
import unittest
from unittest.mock import patch
from fib import fibonacci
class FibonacciTests(unittest.TestCase):
def test_o_primeiro_valor_eh_zero(self):
valor = fibonacci(posicao=0)
self.assertEqual(valor, 0)
def test_o_segundo_valor_eh_um(self):
valor = fibonacci(posicao=1)
self.assertEqual(valor, 1)
@patch('builtins.input', return_value=[
(2, 1), (3, 2), (4, 3), (5, 5), (6, 8),
(7, 13), (8, 21), (9, 34), (10, 55)])
def test_o_proximo_valor_eh_a_soma_dos_dois_anteriores(self, retornos):
for par in retornos():
posicao, valor_esperado = par
valor = fibonacci(posicao)
self.assertEqual(valor, valor_esperado)
def test_deve_retornar_zero_para_posicoes_negativas(self):
valor = fibonacci(-1)
self.assertEqual(valor, 0)
if __name__ == '__main__':
unittest.main()
Todo o código mostrado aqui, e também a sequência de commits realizada pela dupla fictícia, podem ser encontradas neste repositório.
Cuidado com os exageros!
Programar em par é uma atividade intensa, e exige muito das duas pessoas. Exige coordenação e atenção redobrada. Se exagerarmos na dose, pode ser exaustivo. E como a dinâmica ping-pong é uma das mais mais intensas e que mais exigem do programador.
Não se engane: os resultados podem ser incríveis. Inclusive, na melhor sessão de pair programming que experimentei até hoje, apliquei essa dinâmica com meu amigo Fernando Medeiros na Ambev Tech, em 2017. Foi uma experiência ao mesmo tempo fluida e produtiva. E também um tanto cansativa. Mas nós sabíamos que era necesssário manter um balanço saudável nas nossas rotinas para suportar esse nível de trabalho.
É uma ilusão achar que duas pessoas vão conseguir trabalhar em par o dia inteiro (eu mesmo cometi esse erro uns anos antes), pois um programador tem muitas outras atividades para fazer além de programar. Precisa separar tempo para ler e-mails, revisar códigos dos colegas, participar de reuniões, e também para atividades pessoais.
Então a dica é evitar sessões muito longas. Duas ou três horas de ping-pong pair programming seguidas são mais que suficientes para render bons frutos.
Outra dica é planejar pausas de tempos em tempos, para aliviar a tensão. Se possível, faça uma pequena pausa a cada meia hora. Isso vai ajudar a manter a mente tranquila e focada.
Deixe o colega respirar
Existem pessoas impacientes, e existem pessoas pró-ativas. Alguns programadores são ambos, e não conseguem ver o colega parar de digitar por alguns segundos que já correm pra dar pitaco e “ajudar o colega a desempacar”.
É importante ajudar, com certeza, e estar atento caso sua dupla trave, mas muitas vezes, tudo o que o colega precisa é de uns 10 segundos para formular seu pensamento e planejar o próximo passo. É importante dar esse espaço para que a experiência seja agradável para ambos.
Como usar ping-pong pair programming para treinar TDD?
Todo praticante de TDD sabe que o começo é difícil, e para cair a ficha, é necessário treinar muito. E demora mais ainda para que programar desta forma se torne praticamente sua segunda natureza.
Seguir o ciclo do TDD, para quem está começando, é contra-intuitivo. Nosso instinto diz para ir lá e escrever aquele código logo. Ele diz “depois a gente escreve os testes”.
O pareamento ping-pong pode ser usado para combater esse impulso, e treinar TDD até que se torne natural. Há dois cenários em que esta estratégia de pareamento pode ajudar:
- Par entre um praticante experiente em TDD e um iniciante
- Par entre dois iniciantes em TDD
No primeiro cenário, um programador experiente em TDD pode acelerar em muito o aprendizado de um iniciante, com dicas, boas práticas, e discutindo as estratégias de desenvolvimento. Será ótimo para o iniciante observar o modus operandi do colega mais experiente.
Se você é um praticante experiente, então esta é a forma ideal para ensinar outros e passar a prática adiante.
Nem sempre temos um colega experiente em TDD à disposição para parear, mas tudo o que um iniciante empolgado precisa é encontrar outro. Vai ser um pouco mais difícil e vai exigir atenção redobrada, mas os dois podem “vigiar” o processo e colher grandes resultados. Com um tanto de dedicação, aos poucos, o ciclo do TDD se tornará cada vez mais natural.
Finalizando
Espero que este breve artigo inspire algumas pessoas a investir no TDD, e outras ainda a difundir essa prática por aí, como venho fazendo desde que fui introduzido a ela quando comecei a trabalhar na Inventti em 2013. Foi quando virei fã do TDD.