Un switch évolutif en C#

Un billet sur le blog d'Excilys concernant les switchs en java me donne envie de parler un peu de codage en C#.

Tout développeur avec un peu d'expérience sait que les "switch" sont une grande source d'erreurs et une plaie pour la maintenabilité d'un code.
Un switch classique, ça donne quelque chose comme ça :

static class Switch
{
  public static string GetFunctionDescription (string functionName)
  {
    switch (functionName) {
    case "add":
      return "Compute the sum of two numbers";
    case "sub":
      return "Compute the difference of two numbers";
    default:
      throw new ArgumentException ("Unknown function");
    }
  }

  public static int Compute (string functionName, int op1, int op2)
  {
    switch (functionName) {
    case "add":
      return op1 + op2;
    case "sub":
      return op1 - op2;
    default:
      throw new ArgumentException ("Unknown function");
    }
  }
}

Et ça s'utilise comme ça :

Console.WriteLine( Switch.GetFunctionDescription("add") );
Console.WriteLine( "Result={0}", Switch.Compute("add",3,2) );

Dès que l'on veut rajouter un cas, on est presque sûr d'oublier quelque chose.
Alors, le polymorphisme de toute bonne conception orientée objet vient à notre secours et on peut remplacer nos cases par une interface et ses implémentations :

interface Function
{
  string Description { get; }
  int Compute (int op1, int op2);
}

class FunctionAdd : Function
{
  public string Description { get { return "Compute the sum of two numbers"; } }

  public int Compute (int op1, int op2) { return op1 + op2; }
}

class FunctionSub : Function
{
  public string Description { get { return "Compute the difference of two numbers"; } }

  public int Compute (int op1, int op2) { return op1 - op2; }
}

Du coup, on peut se contenter d'un switch unique qui sera bien plus facile à maintenir

static class Switch
{
  public static Function GetFunction (string functionName)
  {
    switch (functionName) {
    case "add":
      return new FunctionAdd();
    case "sub":
      return new FunctionSub();
    default:
      throw new ArgumentException ("Unknown function");
    }
  }
}

Console.WriteLine( Switch.GetFunction("add").Description );
Console.WriteLine( "Result={0}", Switch.GetFunction("add").Compute(3,2) );

Si on est vraiment allergique au mot-clef "switch", on peut aussi utiliser un dictionnaire pour référencer les cas en fonction de leur nom mais ça ne change pas grand chose en termes de maintenabilité. On a autant de classes dérivées que de "case" et on a un point central qui fait l'association entre les valeurs possibles et leur exécuteur.

static class Switch
{
  static Dictionary<string, Function> Functions { get; set; }

  static Switch ()
  {
    Functions = new Dictionary<string, Function> ();
    Functions["add"] = new FunctionAdd ();
    Functions["sub"] = new FunctionSub ();
  }

  public static Function GetFunction (string functionName)
  {
    if (!Functions.ContainsKey (functionName))
      throw new ArgumentException ("Unknown function");
    return Functions[functionName];
  }
}

Mais ce qui serait vraiment intéressant, ce serait d'évacuer complètement la maintenance d'un point central. Pour cela il faudrait pouvoir déclarer la valeur du "case" (dans notre exemple, le nom de la fonction) avec son exécution (dans notre exemple la classe qui implémente la fonction).

.NET offre un concept facile à mettre en oeuvre pour réaliser cela : les attributs.

Ainsi, on déclare un attribut pour nommer nos cas :

class CaseAttribute : Attribute
{
  public string Name { get; private set; }
  public CaseAttribute (string name) { Name = name; }
}

Et on l'utilise pour qualifier chacune de nos implémentations :

[Case("add")]
class FunctionAdd : Function
{
  public string Description { get { return "Compute the sum of two numbers"; } }

  public int Compute (int op1, int op2) { return op1 + op2; }
}

[Case("sub")]
class FunctionSub : Function
{
  public string Description { get { return "Compute the difference of two numbers"; } }

  public int Compute (int op1, int op2) { return op1 - op2; }
}

A partir de là, on peut implémenter un mécanisme générique pour réaliser les switchs, par exemple :

static class Switch<T> where T : class
{
  public static T On (string val)
  {
    var switchSelection = from type in Assembly.GetCallingAssembly ().GetTypes ()
      from attribute in type.GetCustomAttributes (false) as Attribute[]
      let caseAttribute = attribute as CaseAttribute
      where caseAttribute != null && caseAttribute.Name == val
      select type;
    
    if (switchSelection.Count () == 0)
      throw new ArgumentException ("Unknown case");
    
    if (switchSelection.Count () > 1)
      throw new ArgumentException ("Multiple implementations for same case !!!");
    
    return Activator.CreateInstance (switchSelection.First ()) as T;
  }
}

Et l'appel d'un switch reste relativement lisible :

Console.WriteLine( Switch<Function>.On("add").Description );
Console.WriteLine( "Result={0}", Switch<Function>.On("add").Compute(3,2) );

Avec l'implémentation ci-dessus, on est limité à utiliser les cas dans l'assembly où ils sont déclarés mais on pourrait bien sûr imaginer des choses plus évoluées...