[Note : cet article a déjà été posté ici : http://www.nicholassuter.com/2014/07/mon-premier-code-fluent-en-csharp/]

Les API Fluent sont à la mode en ce moment. L’objectif principal est de fournir au client (le développeur) un cadre de travail clair et productif. On en voit par exemple dans Linq, sous forme de méthodes d’extension, ou encore chez les copains de NFluent, une excellente librairie d’assertions pour faciliter l’écriture et la compréhension de vos tests unitaires. Fathi Bellahcene en a d’ailleurs parlé ici et . Et j’ai eu besoin d’en développer une moi-même tout récemment.

Mon problème

Pour commencer, j’ai la méthode suivante :

internal void Enqueue<T>(Dictionary<string, string> inputParameters) where T : IJob
{
  //Faire des trucs
}

Cette méthode, comme son nom l’indique, empile une demande d’exécution d’un job T de type IJob avec sa collection de paramètres. Pas de soucis, ça marche bien, et mon API est claire. Je veux maintenant permettre d’empiler une série de jobs de types hétérogènes (mais héritant tous de IJob). Je ne sais pas combien de jobs non plus, et je désire laisser au client sa liberté sur ce point. Quelles alternatives ai-je ?

1. Des surcharges :

internal void Enqueue<T1, T2>(Dictionary<string, string> inputParameters, Dictionary<string, string> inputParameters2) where T1 : IJob where T2 : IJob
{
  //Faire des trucs
}

internal void Enqueue<T1, T2, T3>(Dictionary<string, string> inputParameters, Dictionary<string, string> inputParameters2, Dictionary<string, string> inputParameters3) where T1 : IJob where T2 : IJob where T3 : IJob
{
  //Faire des trucs
}

… et ainsi de suite (Microsoft craque généralement à 16 pour ce genre de chose. Alors, ça marche, mais ça me fait beaucoup de code redondant (et qui aime ça ?). Ensuite, je limite mon client au nombre de surcharges que j’aurai écrites. A moins que je les fasse générer en T4. Ca peut être une idée, mais bon… Problème aussi : on perd la lisibilité de l’association entre le type de job et ses paramètres. Je vous laisse enfin imaginer la tête de l’Intellisense quand le client tapera “Enqueue”.

2. Je développe un sas

J’étends mon API pour permettre à mon client d’écrire quelque chose comme :

var clientApi = new ClientApi()
client.Prepare<MonIJob1>(params1)
client.Prepare<MonIJob2>(params2)
client.Prepare<MonIJob3>(params3)
client.ExecuteAll()

Alors ça marche, mais c’est redondant et pas très élégant.

3. Une interface fluent

Moi, ce que j’aimerais, c’est que mon client puisse écrire simplement quelque chose comme :

Enqueue.With<MonIJob1>(params1).And<MonIJob2>(params2).And<MonIJob3>(params3).Execute()

Oh, que c’est élégant, que c’est pratique à utiliser ! Mon client n’est pas limité par le nombre de jobs qu’il peut empiler, et il n’a qu’à suivre l’Intellisense pour savoir comment utiliser mon API. Alors, comment on fait ?

Implémentation

Mon point d’entrée est “Enqueue”. Il me faut donc une classe Enqueue, ainsi que des choses à empiler. Et la première méthode à appeler ensuite est With. Il me faut donc une méthode statique With :

using System;
using System.Collections.Generic;
using System.Transactions;
using Scam.Jobs.Core;

namespace Scam.Jobs.ClientApi
{
  public class Enqueue
  {
    private List<JobToEnqueue> _listOfJobs;

    public static Enqueue With<T>(Dictionary<string, string> inputParameters) where T : IJob
    {
      string fullName = typeof(T).AssemblyQualifiedName;
      var listOfJobs = new List<JobToEnqueue> { new JobToEnqueue {JobFullName = fullName, InputParameters = inputParameters} };
      var enqueue = new Enqueue {_listOfJobs = listOfJobs};
      return enqueue;
    }

La seule chose remarquable ici est que la méthode With est statique, initialise la liste des jobs à traiter, et retourne une nouvelle instance de la classe Enqueue. Puisque c’est la seule méthode statique, l’intellisense ne proposera que cette méthode lorsque le client tapera “Enqueue.”.

Vient ensuite la méthode And :

public Enqueue And<T>(Dictionary<string, string> inputParameters) where T : IJob
{
  string fullName = typeof(T).AssemblyQualifiedName;
  _listOfJobs.Add(new JobToEnqueue {JobFullName = fullName, InputParameters = inputParameters});
  return this;
}

C’est une méthode d’instance et elle retourne à nouveau l’instance, ce qui nous permet de chaîner les appels à cette méthode.

Et enfin la méthode Execute() :

public void Execute()
{
  var repository = new JobRepository();

  using (var ts = new TransactionScope())
  {
    foreach (JobToEnqueue jobToEnqueue in _listOfJobs)
    {
      repository.Enqueue(jobToEnqueue.JobFullName, jobToEnqueue.InputParameters);
    }

    ts.Complete();
  }
}

C’est également une méthode d’instance, mais elle ne retourne rien. Cela signifie donc au client que sa commande est complète. Et le tour est joué.

image_thumb.png

 

image.png

Nous avions ici un exemple simple. Dans des cas plus complexes nous aurions tout un arbre de possibilités, et en fonction de l’instance retournée par une méthode, nous aurions de nouvelles possibilités d’enchaînement. La méthode With pourrait rendre des instances de type différent en fonction du type de T, chacune exposant sa propre méthode et ainsi de suite. Dans ce genre de situation, l’important sera de bien modéliser l’arbre des possibles.

Pas de commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *