jueves, 22 de febrero de 2018

Crazy FizzBuzz in C#

Si programas casi seguro que te has encontrado con el test FizzBuzz o has leído alguna reseña o referencia a él; y si no, aquí dejo una de un blog famoso.

Pero, aparte de su principal uso para filtrar profesionales de la programación, me he dado cuenta de que los matices inherentes de este test (condiciones no exclusivas y salida incremental) lo hacer muy bueno para demostrar técnicas que fomenten DRY, separación de responsabilidades y organización de código.


Pongamos una de las implementaciones más sencillas y directas:

function fizzBuzz(){
 for(var i=1;i<=100;i++){
  if(i%5 === 0 && i%3 === 0){
   print('FizzBuzz');
  } else if(i%3 === 0){
   print('Fizz');
  } else if(i%5 === 0){
   print('Buzz');
  } else {
   print(i);
  }
 }
}

Aquí se puede apreciar el pollo montado con los if's y que el módulo se calcula varias veces dado que las condiciones no son exclusivas.

Si limpiamos un poco la cosa nos queda así:

function fizzBuzz(){
 var output, currentNumberDivisibleBy3, currentNumberDivisibleBy5;
 for(var i=1;i<=100;i++){
  output = '';
  currentNumberDivisibleBy3 = (i%3 === 0);
  currentNumberDivisibleBy5 = (i%5 === 0);
  if(currentNumberDivisibleBy3){
   output+='Fizz';
  } 
  if(currentNumberDivisibleBy5){
   output+='Buzz';
  } 
  if(!currentNumberDivisibleBy3 && !currentNumberDivisibleBy5) {
   output+=i;
  }
  print(output);
 }
}

Y aun así queda feo de cojones... por eso me parece perfecto para poner ejemplos de técnicas que quiten de en medio esos if's y dejen a la función FizzBuzz su única responsabilidad real: coordinar condiciones y mantener una salida incremental sin tener que conocer esas condiciones ni que tipo esta manejando.

Usando génericos y técnicas de programación funcional proporcionadas por C#, podemos hacer una función que acepte cualquier tipo como valor a condicionar y aplique cualquier condición que queramos. (Nota: el formateador de codigo no procesa correctamente la notacion de genericos de C# por lo que lo pongo en formato VB aunque el resto de codigo sea C#)

private static void FizzBuzz(Of T)(Func(Of T, bool) fizzCond, Func(Of T, bool) buzzCond, Func(Of T, string) defaultValue, T value)
{
  string output = string.Empty;
 
  bool IsFizz()
  {
    if (fizzCond(value)) { output += "Fizz"; return true; }
    return false;
  }
 
  bool IsBuzz()
  {
    if (buzzCond(value)) { output += "Buzz"; return true; }
    return false;
  }
 
  Console.WriteLine(IsFizz() | IsBuzz() ? output : defaultValue(value));
} 

  • La plantilla de tipo T permite aceptar cualquier tipo para los parámetros.
  • Las condiciones buzz y fizz de los parámetros de entrada son funciones de primer orden que se le pasan por parámetro. Esto permite aplicarlas y coordinarlas sin tener que conocer su implementación.
  • La función de primer orden defaultValue permite obtener el valor que se aplica a la salida si no cumple ninguna condicion Buzz o Fizz sin tener que discriminar por tipos.
  • Las funciones locales IsBuzz e IsFizz dividen los bloques if's por responsabilidad y evitan tener que crear parámetros de salida aparte de su valor de retorno. Esto también permite aplicar el OR no cortocircuitado para ir montando la salida incremental u obtener su valor por defecto en caso de que no se cumpla ninguna condición.
    Pero además también hay truquillos funcionales para el uso de esta función.

private static Func IsModOf(int divisor){
  return dividendo => dividendo % divisor == 0;
}

var isModOf3 = IsModOf(3);
var isModOf5 = IsModOf(5);

for (int i = 1; i < 17; i++)
{
  FizzBuzz(Of int)(isModOf3, isModOf5, value => value.ToString(), i);
}


La función IsModOf nos permite crear diferentes condiciones para Buzz y Fizz retornando como valor una función forjada en tiempo de ejecución con el valor de su parámetro de entrada (divisor). También creamos la función lambda para el parámetro defaultValue; ya que es, en este punto, donde sabemos con que tipo estamos trabajando; por lo que es parte de su responsabilidad.

Gracias a esto podemos utilizar FizzBuzz para cualquier tipo que se nos presente y las responsabilidades estan completamente definidas y segregadas.

Un ejemplo con char's:

private static Func IsEqualsThan(char letter)
{
  return inputLetter => inputLetter.Equals(letter);
}

char[] letras = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K' };
var isEqualsThanF = IsEqualsThan('F');
var isEqualsThanB = IsEqualsThan('B');

foreach (var item in letras)
{
  FizzBuzz(Of char)(isEqualsThanF, isEqualsThanB, value => value.ToString(), item);
}

Las características funcionales de C# permiten generar código con claridad y responsabilidades segregadas sin tener que tirar todo el código de infraestructura técnica que sería necesario si sólo tuvieramos OOP. Y con el test FizzBuzz como ejemplo se puede apreciar muy fácilmente y aprender de ello.

No hay comentarios:

Publicar un comentario