Guida FizzBuzz Kata Parte 2 – Siamo arrivati alla nostra seconda puntata di questo nostro articolo sul Test Driven Development.

Nel primo articolo abbiamo iniziato a lavorare al FizzBuzz kata e abbiamo visto come sviluppare con la tecnica del tdd.

Oggi ci concentreremo in una prima parte di refactoring in cui vedremo quanto possono essere utili i test e successivamente ci concentreremo nello sviluppo della seconda parte del kata.

Guida FizzBuzz Kata: Il refactoring

Questa è la nostra funzione al termine del primo articolo:

def say_number(number):
    if (number % 15) == 0:
        return 'fizzbuzz'
    if (number % 3) == 0:
        return 'fizz'
    if (number % 5) == 0:
            return 'buzz'
    return str(number)

Funzione esplicativa

Indubbiamente le espressioni all’interno dei nostri if rendono poco leggibile il codice, in un primo passo possiamo pensare di creare una funzione di supporto “is_divisible_by” che prende come parametri due numeri

def is_divisible_by(dividend, divider):
    return dividend % divider == 0

La funzione in questo caso è triviale, come nelle espressioni all’interno dell’if andiamo a controllare che il resto della divisione sia zero in modo da capire se un numero è un multiplo di un altro.

Utilizziamo la nostra nuova funzione nella nostra “say_number”

def say_number(number):
    if is_divisible_by(number, 15):
        return 'fizzbuzz'
    if is_divisible_by(number, 3):
        return 'fizz'
    if is_divisible_by(number, 5):
        return 'buzz'
    return str(number)

Eseguendo i test possiamo vedere che il comportamento della nostra funzione rimane invariato.

Rimozione degli if

Un if è un punto in cui il programma può avere 2 comportamenti differenti, questo obbliga il lettore a dover capire cosa fà ogni strada percorribile. È sempre preferibile, per una questione di chiarezza del ridurre gli if al minimo indispensabile.

Possiamo rimuovere gli if andando a creare una funzione di mappatura che mappi un divisore al suo suono specifico.

from functools import reduce

def get_sound(number):
    mappers = [
        {'number': 3, 'sound': 'fizz'},
        {'number': 5, 'sound': 'buzz'},
    ]
    return reduce(
        lambda acc, mapper: acc + mapper['sound'],
        filter(
            lambda mapper: is_divisible_by(number, mapper['number']),
            mappers
        ),
        ""
    )

Il codice qui diventa un po’ più complesso ma nulla di arcano, la funzione “filter” restituisce solo i mappatori che hanno il parametro “number” che divide il numero dato, la reduce unisce tutte le stringhe dei suoni.

Avendo sviluppato prima i test, questi ci consentiranno di poter provare velocemente se il nostro codice è corretto.

Per esempio durante lo sviluppo di questo kata ho scoperto che la reduce in python ha un terzo parametro per indicare il valore iniziale dell’accumulatore. I test infatti non venivano superati e questo mi ha permesso anche di scoprire nuove cose sul linguaggio che stavo utilizzando senza generare bug in produzione.

Aggiorniamo anche il codice della funzione “say_number”

def say_number(number):
    return get_sound(number) or str(number)

Test superati, possiamo andare al secondo punto del kata.

Guida FizzBuzz Kata – Nuove richieste

Arriviamo quindi al secondo punto del kata ed andiamo ad aggiornare i nostri nuovi criteri di accettazione

Criteri di accettazione

  1. se il numero passato in input non è multiplo di 3 e/o 5 o non li contiene allora la funzione dovrà restituire quel numero
  2. se il numero passato in input è un multiplo di 3i o lo contiene allora la funzione tornerà la stringa ‘fizz’
  3. se il numero in input è moltiplo di 5i o lo contiene la funzione tornerà la stringa ‘buzz’
  4. se il numero in input è multiplo di 3 e 5 o li contiene la funzione tornerà ‘fizzbuzz’

Modifiche

Con il refactoring che abbiamo eseguito portare a termine le queste nuove richieste è molto più semplice ma andiamo con ordine.

Scriviamo i nostri nuovi test:

class TestSayNumber(unittest.TestCase):
    def test_return_number_string(self):
        self.assertEqual(say_number(1), '1')

    def test_return_fizz(self):
        self.assertEqual(say_number(3), 'fizz')
        self.assertEqual(say_number(6), 'fizz')
        self.assertEqual(say_number(9), 'fizz')
        self.assertEqual(say_number(12), 'fizz')
        self.assertEqual(say_number(13), 'fizz')
        self.assertEqual(say_number(31), 'fizz')
        self.assertEqual(say_number(1234), 'fizz')

    def test_return_buzz(self):
        self.assertEqual(say_number(5), 'buzz')
        self.assertEqual(say_number(10), 'buzz')
        self.assertEqual(say_number(20), 'buzz')
        # self.assertEqual(say_number(35), 'buzz') removed
        self.assertEqual(say_number(52), 'buzz')
        self.assertEqual(say_number(152), 'buzz')
        self.assertEqual(say_number(5254), 'buzz')

    def test_return_fizzbuzz(self):
        self.assertEqual(say_number(15), 'fizzbuzz')
        self.assertEqual(say_number(30), 'fizzbuzz')
        self.assertEqual(say_number(60), 'fizzbuzz')
        self.assertEqual(say_number(53), 'fizzbuzz')
        self.assertEqual(say_number(1354), 'fizzbuzz')

Da notare il test rimosso, il 35 con le nuove richieste deve restituire fizzbuzz.

Per superare i nostri nuovi test basta andare a creare una nuova funzione per verificare l’inclusione di un numero all’interno di un altro:

def contain_number(container, part):
    return str(part) in str(container)

E aggiungerlo alla nostra funzione di mappatura:

def get_sound(number):
    mappers = [
        {'number': 3, 'sound': 'fizz'},
        {'number': 5, 'sound': 'buzz'},
    ]
    return reduce(
        lambda acc, mapper: acc + mapper['sound'],
        filter(
            lambda mapper: is_divisible_by(number, mapper['number']) or contain_number(number, mapper['number']),
            mappers
        ),
        ""
    )

Superiamo i test e concludiamo il kata.

Conclusioni sulla seconda parte della guida al FizzBuzz Kata

I test ci possono indicare la via e ci danno una rete di sicurezza mentre sviluppiamo.

Il tdd ci insegna che prima di pensare di scrivere il codice è bene chiarire cosa vogliamo dalle nostre funzioni e previene il procastinaggio dello scrivere i test.