Art ASCII et jokers
Série 8

Introduction

Le but de cette série est double : premièrement, de vous faire utiliser le patron de conception Decorator pour dessiner des « images » textuelles, et deuxièmement de vous faire utiliser les jokers. Elle est donc séparée en deux parties, que vous pouvez faire dans l'ordre que vous préférez.


Partie 1 : art ASCII

Cette première partie a pour but de mettre en œuvre une bibliothèque simple de dessin d'images constituées de caractères, ce que l'on nomme parfois « art ASCII » (ASCII art).

Pour simplifier les choses, les images de cette bibliothèque sont rectangulaires et décrites par l'interface ci-dessous :

public interface TextImage {
  public int width();
  public int height();
  public List<String> drawing();

  default public void printOn(PrintStream s) {
    drawing().forEach(s::println);
  }
}

Les méthodes width et height donnent la largeur et la hauteur de l'image, en caractères, et drawing donne le contenu de l'image sous la forme d'une liste immuable de chaînes qui sont les lignes de l'image. Cette liste doit avoir un nombre d'éléments égal à la hauteur de l'image, et chacun de ces éléments doit être une chaîne de longueur égale à la largeur de l'image.

Par exemple, l'image ci-dessous :

baba
baba
baba

a une largeur de 4 (caractères), une hauteur de 3 (lignes) et son dessin est une liste contenant trois fois la chaîne baba.

La méthode par défaut printOn permet d'imprimer l'image à laquelle on l'applique sur un flot de sortie de type PrintStream. Par exemple, pour dessiner une image sur la console, il suffit d'appeler cette méthode en lui passant System.out en argument.

Pour démarrer cette série, nous mettons à votre disposition une archive Zip contenant l'interface TextImage ci-dessus ainsi qu'une classe utilitaire nommée Strings contenant des méthodes de manipulation de chaînes de caractères. Avant d'aller plus loin, importez cette archive dans votre projet puis familiarisez-vous avec le code fourni.

Exercice 1 : images de base

Avant de pouvoir définir des décorateurs, il faut bien entendu définir quelques images de bases qui pourront ensuite être décorées.

Il vous est demandé de définir les deux types d'images textuelles de base suivants :

  1. une image obtenue à partir d'une chaîne de caractères, dont la largeur est égale à la longueur de la chaîne et la hauteur est 1,
  2. une image de largeur et de hauteur donnée, composée uniquement d'un caractère donné qui remplit tout l'image.

Bien entendu, à chacun de ces types d'images de base correspond une classe implémentant l'interface TextImage et donnant une définition appropriée de ses trois méthodes abstraites.

Une fois ces deux classes définies, ajoutez deux méthodes statiques dans l'interface TextImage simplifiant la création de leurs instances. Par exemple, pour créer une image à partir d'une chaîne de caractères, ajoutez une méthode statique nommée fromString qui, étant donnée une chaîne de caractères, retourne une image dont le dessin est la chaîne en question. Ces méthodes devraient pouvoir s'utiliser ainsi :

// Permet d'obtenir l'image 20x1 :
// La malade pédala mal
TextImage.fromString("La malade pédala mal");

// Permet d'obtenir l'image 3x2 :
// ***
// ***
TextImage.filled(3, 2, '*');

Exercice 2 : décorateurs

Les images de base étant définies, il est temps de passer aux décorateurs, qui permettent d'obtenir une nouvelle image par transformation d'une image existante.

Il vous est demandé de définir les deux décorateurs suivants :

  1. un décorateur permettant de faire une symétrie horizontale d'une image,
  2. un décorateur permettant de transposer une image, c-à-d d'inverser le rôle de ses lignes et de ses colonnes, exactement comme lors de la transposition d'une matrice.

Une fois ces deux classes définies, ajoutez deux méthodes par défaut à l'interface TextImage simplifiant la création de leurs instances. Ces méthodes devraient pouvoir s'utiliser ainsi :

// Permet d'obtenir l'image 20x1 :
// lam aladép edalam aL
TextImage.fromString("La malade pédala mal")
  .flippedHorizontally();

// Permet d'obtenir l'image 1x3 :
// é
// t
// é
TextImage.fromString("été").transposed();

Exercice 3 : composites

En plus des décorateurs, qui permettent d'appliquer une transformation aux images, il est intéressant de définir des composites, qui permettent de composer plusieurs images existantes pour en obtenir une nouvelle.

Cette technique de composition est décrite par le patron Composite, très proche du patron Decorator. Il n'a pas encore été examiné au cours, mais est discuté dans les notes de cours sur les patrons de conception. Cela dit, l'idée est simple et devrait être compréhensible même sans connaître le patron Composite.

Il vous est demandé de définir les deux compositions suivantes :

  1. la composition « côte à côte » qui compose deux images en plaçant la première à gauche de la seconde ; les deux images peuvent avoir une hauteur différente, auquel cas des espaces sont insérées en bas de l'image la moins haute,
  2. la composition « l'une sur l'autre » qui compose deux images en plaçant la première au dessus de la seconde (verticalement) ; les deux images peuvent avoir une largeur différente, auquel cas des espaces sont ajoutées à droite de l'image la moins large.

Une fois ces deux classes définies, ajoutez deux méthodes par défaut à l'interface TextImage simplifiant la création de leurs instances. Ces méthodes devraient pouvoir s'utiliser ainsi :

// Permet d'obtenir l'image 18x2 :
// Un rectangle : ###
//                ###
TextImage.fromString("Un rectangle : ")
  .leftOf(TextImage.filled(3, 2, '#'));

// Permet d'obtenir l'image 4x3 :
// XXX 
// OOOO
// OOOO
TextImage.filled(3, 1, 'X')
  .above(TextImage.filled(4, 2, 'O'));

Exercice 4 : dessin d'un échiquier

Utilisez aussi judicieusement que possible les méthodes ajoutées à l'interface TextImage pour définir l'image d'un échiquier ci-dessous :

+------------------------+
|###   ###   ###   ###   |
|###   ###   ###   ###   |
|   ###   ###   ###   ###|
|   ###   ###   ###   ###|
|###   ###   ###   ###   |
|###   ###   ###   ###   |
|   ###   ###   ###   ###|
|   ###   ###   ###   ###|
|###   ###   ###   ###   |
|###   ###   ###   ###   |
|   ###   ###   ###   ###|
|   ###   ###   ###   ###|
|###   ###   ###   ###   |
|###   ###   ###   ###   |
|   ###   ###   ###   ###|
|   ###   ###   ###   ###|
+------------------------+

Pour dessiner le cadre autour de cet échiquier, ajoutez encore une méthode de décoration à l'interface TextImage, nommée p.ex. framed et permettant d'ajouter un cadre autour de l'image à laquelle on l'applique. Contrairement aux autres méthodes, celle-ci s'exprime assez facilement au moyen de méthodes existantes, et il n'est donc pas nécessaire de définir une classe pour les images encadrées.


Partie 2 : jokers

Comme vu au cours, les jokers (wildcards) permettent, entre autres, de généraliser le type des paramètres de certaines méthodes génériques. Ainsi, une méthode addAll qui ajoute à la liste à laquelle on l'applique tous les éléments de la liste reçue en argument peut être définie ainsi :

interface List<E> {
  // ...
  void addAll(List<? extends E> other);
}

Comparée à une version prenant une liste de type List<E> en argument, celle-ci est plus générale puisqu'elle permet, p.ex., l'appel suivant :

List<Number> l = new LinkedList<>();
List<Integer> li = new LinkedList<>();
li.add(1);
l.addAll(li);

Cet appel serait invalide si addAll prenait un argument de type List<E>. Pour plus de détails, voir le cours.

Exercice

Afin de vous entraîner à utiliser les jokers, nous vous fournissons une archive Zip contenant un fichier Lists.java définissant une classe Lists (notez le s à la fin !) qui offre trois méthodes statiques travaillant sur les listes Java :

  1. addMany, qui ajoute un certain nombre d'occurrences d'un élément à la fin d'une liste,
  2. copy, qui copie le contenu d'une première liste à la fin d'une seconde liste,
  3. max, qui retourne le plus grand élément d'une liste selon un comparateur reçu en argument.

Nous vous fournissons de plus un test JUnit pour cette classe dans le fichier ListsTest.java. Ce test comporte 13 appels aux trois méthodes susmentionnées, numérotés par des commentaires. Dans la version que nous vous fournissons, tous ces appels sont signalés comme erronés par Eclipse.

Examinez chacun de ces 13 appels et déterminez s'il pourrait être valide ou non. Si oui, généralisez le type de la fonction appelée (dans la classe Lists) au moyen de jokers. Si non, mettez l'appel en commentaire et justifiez votre choix. Attention, vous n'avez pas le droit de modifier le code des fonctions de Lists, seulement le type de leurs arguments et variables locales.