Représentation d’une liste en String

A l’occasion d’un entretient, on m’a proposé de développer une méthode simple. L’idée n’était pas tant de coder la fonctionnalité que d’expliquer le raisonnement. Dans la suite, je ne vais pas vous expliquer comment bien programmer, en partant des tests, car j’ai déjà fais un billet intitulé « Kata Digital Romain » à ce sujet. A la place, je vais vous proposer plusieurs solutions qui me semble intéressantes, en Java standard, à l’aide de Guava, ou encore grâce à une approche fonctionnelle.

Le but de l’exercice est de transformer une liste de String en une représentation sous forme d’une seule String dans laquelle les éléments sont simplement séparés par une virgule.

Par exemple, on part de la liste suivante :

1
final List<String> list = Arrays.asList("un", "deux", "trois");

Et on veut un résultat sous la forme d’une chaîne de caractères :

1
final String attendu = "un, deux, trois";

Si on traduit ça en test unitaire, ça va donner quelque chose comme ça :

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testSimple() {
    // Arrange
    final List<String> list = Arrays.asList("un", "deux", "trois");
    final String attendu = "un, deux, trois";
   
    // Act
    final String result = concat(list);
   
    // Assert
    assertEquals(attendu, result);
}

Au passage, vous remarquez le formalisme AAA (Arrange-Act-Assert) utilisé pour présenter ce test. On va compléter (en factorisant) pour tester d’autres valeurs intéressantes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testSimple() {
    // Arrange
    final List<String> list = Arrays.asList("un", "deux", "trois");
    final String attendu = "un, deux, trois";

    // Act and Assert
    doTest(list, attendu);
}

private void doTest(final List<String> list, final String attendu) {
    // Act
    final String result = concat(list);

    // Assert
    assertEquals(attendu, result);
}

Et en ce qui concerne les valeurs spéciales, je pense surtout à des listes vides ou carrément nulles :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testListeVide() {
    // Arrange
    final List<String> list = Arrays.asList();
    final String attendu = "";

    // Act and Assert
    doTest(list, attendu);
}

@Test(expected = IllegalArgumentException.class)
public void testListeNulle() {
    // Arrange
    final List<String> list = null;
    final String attendu = "";

    // Act and Assert
    doTest(list, attendu);
}

Pour compiler une première version simple, j’ai simplement écris une méthode qui ne fait rien :

1
2
3
public static String concat(List<String> list) {
    throw new UnsupportedOperationException("Cette fonction n'est pas encore disponible.");
}

Et quand on lance les tests, tout est bien rouge…

Pour le codage, c’est facile. On prend les tests un peu dans l’ordre qui nous intéresse. Personnellement j’aime bien commencer par les cas qui font sortir :

1
2
3
4
5
6
7
8
public static String concat(List<String> list) {
   
    if(list == null) {
        throw new IllegalArgumentException("Ca ne marche pas avec des listes nulles");
    }
   
    throw new UnsupportedOperationException("Cette fonction n'est pas encore disponible.");
}

Et hop, un premier test qui passe au vert. Pour la suite, je ne vais même pas m’embêter à faire un cas spécial pour les listes vides. Ça sera pris directement en compte par l’algorithme :

1
2
3
4
5
6
7
8
9
10
public static String concat(List<String> list) {
   
    if(list == null) {
        throw new IllegalArgumentException("Ca ne marche pas avec des listes nulles");
    }
   
    final StringBuilder sb = new StringBuilder();
   
    return sb.toString();
}

Et hop, un second test qui devient vert.

Ensuite, il suffit de parcourir la liste et de concaténer les éléments :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static String concat(List<String> list) {
   
    if(list == null) {
        throw new IllegalArgumentException("Ca ne marche pas avec des listes nulles");
    }
   
    final StringBuilder sb = new StringBuilder();
   
    for(final String s : list) {
        sb.append(s);
        sb.append(", ");
       
    }
   
    return sb.toString();
}

Je relance mon test et hop… Bah non, pas hop, car ça ne marche pas : il y a un délimiteur en trop à la fin. Oh zut…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static String concat(List<String> list) {
   
    if(list == null) {
        throw new IllegalArgumentException("Ca ne marche pas avec des listes nulles");
    }
   
    final StringBuilder sb = new StringBuilder();
   
    boolean premier = true;
    for(final String s : list) {
        if(!premier) {
            sb.append(", ");
        } else {
            premier = false;
        }
       
        sb.append(s);
    }
   
    return sb.toString();
}

Et voilà, ça passe au vert. Du coup on va en rester là pour l’instant. Il y aurait encore à dire mais je vous garde ça pour plus tard…

Vous savez que j’adore la bibliothèque Guava. J’ai d’ailleurs fais une tournée des JUG dans laquelle je décris un cas assez similaire. Je vous laisse regarder les slides et écouter les enregistrements. Ils sont disponibles gratuitement sur icauda.com.

Avec Guava, donc, c’est encore plus simple :

1
2
3
4
5
6
7
8
public static String concat(List<String> list) {
   
    if(list == null) {
        throw new IllegalArgumentException("Ca ne marche pas avec des listes nulles");
    }
   
    return Joiner.on(", ").join(list);
}

On pourra aussi s’amuser à avoir des éléments vides ou nuls dans la liste. On peut gérer ça à l’aide des méthodes « skipNulls() » et « useForNull() ». C’est ce dernier choix que je vais vous montrer car il me semble important de noter qu’un élément nul n’a pas pu arriver par hasard dans la liste :

1
return Joiner.on(", ").useForNull("noname").join(list);

Mais, surtout, je vous conseille l’excelent article de Yann Caron, intitulé « Fonction Object Design pattern, en attendant les lambdas de Java 8″. Cela va vous permettre de faire cela de manière fonctionnelle, en Java standard, sans utiliser de bibliothèque.

Attention aux yeux, ça pique un peu :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FunctionalListDecorator<String> fstrings = new FunctionalListDecorator<String>(list);

//Predicate
Predicate<Arguments2<String, Integer>> ignoreEmpty = new Predicate<Arguments2<String, Integer>>() {
 
    @Override
    public Boolean invoke(Arguments2<String, Integer> arguments) {
        return !"".equals(arguments.getArgument1());
    }
};
 
Predicate<Arguments2<String, Integer>> ignoreNull = new Predicate<Arguments2<String, Integer>>() {
 
    @Override
    public Boolean invoke(Arguments2<String, Integer> arguments) {
        return arguments.getArgument1() != null;
    }
};
1
2
3
4
5
6
7
Function<String, Arguments2<String, String>> concat = new Function<String, Arguments2<String, String>>() {
 
    @Override
    public String invoke(Arguments2<String, String> arguments) {
        return arguments.getArgument1() + ", " + arguments.getArgument2();
    }
};
1
String resultat = fstrings.filter(ignoreNull).filter(ignoreEmpty).reduce(concat);

Merci d’avoir lu jusqu’ici ;-)

Laisser un commentaire