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 :
- 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,
- 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 :
- un décorateur permettant de faire une symétrie horizontale d'une image,
- 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 :
- 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,
- 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 :
addMany
, qui ajoute un certain nombre d'occurrences d'un élément à la fin d'une liste,copy
, qui copie le contenu d'une première liste à la fin d'une seconde liste,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.