image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Factory et Builder

Présentation

Factory pattern

Exemple 1 : un gestionnaire de notes

Notre gestionnaire doit pouvoir gérer des notes présentes dans un fichier local, une base de données ou un service web.
On peut donc avoir une classe ancêtre NoteBackend (gestion du stockage de notes) avec les classes dérivées suivantes :

On peut créer une Factory pour initialiser le backend approprié :

public class NoteBackendFactory
{
	/** Create a new note backend
	 * The location parameter is an URL and according to the scheme, the most adapted backend is selected:
	 * file://... : a file backend is chosen
	 *	db:// : a database backend is employed
	 *	http:// : a web service backend is used
	 */
	public NoteBackend createBackend(String location)
	{
		if (location.startsWith("file://")
		{
			...
		} else if (location.startsWith("db://")
		{
			...
		} else if (location.startsWith("http://"))
		{
			...
		}
	}
}

On peut toutefois noter que certains backends nécessitent potentiellement des paramètres supplémentaires.
C'est le cas par exemple des backends base de données et service web qui requièrent l'utilisation d'identifiants (utilisateur, mot de passe) contrairement au backend de fichier local.

On peut donc modifier la Factory pour s'y adapter :

public class NoteBackendBuilder
{
	private final String location; 
	private String login = null;
	private String password = null;

	public NoteBackendFactory(String location)
	{
		this.location = location;
	}

	public void setCredentials(String login, String password)
	{
		this.login = login;
		this.password = password;
	}

	public NoteBackend create()
	{
		...
	}

On dérive alors d'un pattern Factory vers un pattern Builder puisque nous avons ajouté une méthode afin de paramétrer le backend créé. Un des intérêts du Builder est de pouvoir ajouter ultérieurement de nouvelles méthodes (setters) pour définir des paramètres optionnels sans avoir à changer le code existant.

Il est possible également de créer une hiérarchie de builders, i.e. différents builders liés par des relations d'héritage. Nous pourrions par exemple avoir les builders suivants :

Il serait même envisageable de créer un méta-builder qui serait un builder de builder...

Comme pour tous les patterns, il faut toutefois rester parcimonieux : l'usage du pattern ne doit pas complexifier l'architecture mais la rendre plus flexible, modulaire et évolutive. Si notre application est destinée à ne fonctionner qu'avec un seul système de stockage, l'utilisation d'une Factory ou d'un Builder pourrait paraître superflue.

Exemple 2 : StringBuilder

Exemple pratique : construisons une chaînes contenant les entiers de 1 à 100

StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 99; i++)
	sb.append(i).append(",");
String s = sb.append(100).toString();

Clonage d'objet (prototype)

Présentation

Exemple : clonage de brebis

public class SheepCloning
{
	public interface Animal
	{
		public char[] getDNA();
	}
	
	public class Sheep implements Animal
	{
		protected char[] dna ;
		…
		public char[] getDNA() { return dna ; }
	}
	
	public class CloneableSheep extends Sheep implements Cloneable
	{
		CloneableSheep shallowClone()
		{
			return (CloneableSheep)super.clone() ;
		}

		/** Return a deep clone */
		CloneableSheep clone()
		{
			CloneableSheep sheep = (CloneableSheep)super.clone() ;
			sheep.dna = new char[this.dna.length] ;
			// we copy the DNA array
			System.arraycopy(this.dna, 0, sheep.dna, 0, this.dna.length) ;
			return sheep ;
		}
	}
	
	public static void main(String[] args)
	{
		Sheep dolly = … ; // Can be created with a factory
		Sheep dolly2 = (Sheep)dolly.shallowClone() ;
		Sheep dolly3 = (Sheep)dolly.clone() ;
		Virus virus = ...; // We create a nasty virus
		virus.attack(dolly3) ; // The virus only modifies the DNA of dolly3 (for a deep cloning)
		virus.attack(dolly2) ; // The virus modifies the shared DNA of dolly2 and dolly
	}

Singleton

Exemple : une Factory singleton

public class NoteBackendFactory
{
	private static NoteBackendFactory singleton;
	
	/** We create the instante only the first time it is requested */
	public static NoteBackendFactory getInstance()
	{
		if (singleton == null)
			singleton = new NoteBackendFactory();
		return singleton;
	}
	
	/** The constructor is declared private to force the use of the singleton;
	 * thus a user could not accidentely use the constructor
	 */
	private NoteBackendFactory()
	{
	}
	
	public NoteBackend create()
	{
		...
	}

Remarque : l'exemple proposé ne fonctionne que dans un contexte d'utilisation avec une seule thread. Si plusieurs threads exécutent la méthode getInstance() simultanément, il y a un risque que deux instances du singleton soient créées ! Il est donc plus prudent dans ce cas de réécrire ainsi la méthode getInstance() :

public static NoteBackendFactory getInstance()
{
	private static volatile NoteBackendFactory singleton; // we add the volatile keyword to force field update accross threads
	if (singleton == null)
	{
		synchronized(NoteBackendFactory.class)
		{
			// We do a second check protected in a synchronized section to be sure than a second thread has not already initialized the instance
			if (singleton == null)
				singleton = new NoteBackendFactory();
		}
		return singleton;
	}
}

Multiton

Recyclage

Présentation

Exemple : fenêtre d'un terminal

Pour modéliser la fenêtre d'affichage d'un terminal, on créé une classe TextWindow contenant une matrice de lettres DisplayedChar. Plutôt que de créer plusieurs fois les DisplayedChar, nous les crééons une unique fois et nous utilisons un setter pour changer leur contenu. DisplayedChar représente donc une cellule d'affichage pouvant changer.

public class DisplayedChar
{
	private char c; // character
	private Style s; // style of display (bold, striken, underlined, color...)
	
	public char getChar() { return c; }
	public Style getStyle() { return s; }
	
	public void setContent(char character, Style style)
	{
		this.c = character;
		this.s = style;
	}
}

public class TextWindow
{
	private DisplayedChar[][] textMatrix;
	
	public TextWindow(int w, int h)
	{
		this.textMatrix = new DisplayedChar[w][h];
		// we create a DisplayedChar for each cell
		for (int i = 0; i < h; i++)
			for (int j = 0; j < w; j++)
				this.textMatrix[i][j] = new DisplayedChar(); // create using the default constructor
	}
	
	public void setText(int x, int y, String text, Style style)
	{
		for (int i = 0; i < text.length(); i++)
			if (x + i < textMatrix[y].length)
				textMatrix[y][x + i].setContent(text.charAt(i), style);
	}
}

Maintenant, imaginons qu'il soit possible "d'effacer" des cellules (on met la valeur d'une cellule de la matrice à null) ; on pourra alors conserver les DisplayedChar enlevées de la matrice dans une liste en vue de recyclage :

public class TextWindow2
{
	private DisplayedChar[][] textMatrix;
	private List<DisplayedChar> charPool;
	
	public TextWindow2(int w, int h)
	{
		// we create a matrix filled with null references
		this.textMatrix = new DisplayedChar[w][h];
		this.charPool = new ArrayDeque<DisplayedChar>();
	}
	
	/** This method could have been moved to the DisplayedChar class
	 * with the charPool as a static member
	 */
	private DisplayedChar createDisplayedChar(char character, Style style)
	{
		DisplayedChar dc = null;
		if (charPool.size() > 0)
		{
			// we recycle an existing displayed char
			dc = charPool.remove(0); // remove the object at the beginning of the list, the object that has been waiting the most to be recycled
		} else
		{
			dc = new DisplayedChar();
		}
		dc.setContent(character, style);
		return dc;
	}
	
	/** Clear a line from the text matrix */
	public void clearLine(int y)
	{
		for (int i = 0; i < textMatrix[y].length; i++)
			if (textMatrix[y][i] != null)
			{
				charPool.add(textMatrix[y][i]); // add the object in the recycling pool
				textMatrix[y][i] = null;
			}
	}
	
	/** Modify the text matrix by putting a text at a given position on the grid */
	public void setText(int x, int y, String text, Style style)
	{
		for (int i = 0; i < text.length(); i++)
			if (x + i < textMatrix[y].length)
				textMatrix[y][x + i] = createDisplayedChar(text.charAt(i), style);
	}
}

Injection de dépendance

Si nous utilisons Guice, nous devons déclarer cette bibliothèque comme dépendance ; par exemple dans le pom.xml en utilisant Maven :

<dependency>
    <groupId>com.google.inject</groupId>
    <artifactId>guice</artifactId>
    <version>5.1.0</version>
</dependency>

Pour construire une instance d'une classe, nous avons besoin d'initialiser ses champs. On fait appel pour cela à l'injecteur avec l'annotation @Inject. Nous pouvons utiliser cette annotation dans trois contextes :

Prenons pour exemple une classe chargée d'afficher un Hello World personnalisé :

public class HelloPrinter {
	// injection sur un champ
	@Inject
	private String name;

	private final MessageDisplayer displayer;

	private final Random random;

	private int displayMinTimes;

	private int displayMaxTimes;

	// injection par constructeur
	@Inject
	public HelloPrinter(MessageDisplayer displayer, Random random) {
		this.displayer = displayer;
		this.random = random;
	}

	// injection par setter
	@Inject
	public void setDisplayTimes(int displayMinTimes, int displayMaxTimes) {
		this.displayMinTimes = displayMinTimes;
		this.displayMaxTimes = displayMaxTimes;
	}

	public void sayHello() {
		int i = random.nextInt(displayMinTimes, displayMaxTimes);
		IntStream.range(0, i).forEach(x -> {displayer.display(x + ": Hello World " + name);});
	}
}

Généralement, il est préférable de rendre les champs figés avec final et d'utiliser uniquement une injection sur le constructeur ; l'exemple précédent utilise les trois types d'injection à titre d'illustration des possibilités d'injection.

MessageDisplayer est une interface que nous avons écrite pour afficher un message :

public interface MessageDisplayer {
    void display(String message);
}

On peut implanter différentes classes utilisant l'interface MessageDisplayer, par exemple une classe affichant sur System.out et une autre sur System.err. Ces implantations sont en visibilité package :

@Singleton
class StdoutMessageDisplayer implements MessageDisplayer {
    public void display(String message) {
        System.out.println(message);
    }
}

@Singleton
class StderrMessageDisplayer implements MessageDisplayer {
    public void display(String message) {
        System.err.println(message);
    }
}

Notons l'annotation @Singleton qui signifie que si l'injecteur utilise ces classes pour l'injection, il n'utilisera qu'une unique instance dans tous le programme (sinon une instance différente serait créée par défaut à chaque site d'injection).

Un problème demeure : comment l'injecteur peut-il savoir s'il doit injecter StdoutMessageDisplayer ou StderrMessageDisplayer à l'endroit où un MessageDisplayer est nécessaire ?

Pour cela, il est nécessaire de configurer l'injecteur avec un module :

class HelloPrinterInjectionModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(MessageDisplayer.class).to(StdoutMessageDisplayer.class);
        bind(MessageDisplayer.class).annotatedWith(Names.named("stdout")).to(StdoutMessageDisplayer.class);
        bind(MessageDisplayer.class).annotatedWith(Names.named("stderr")).to(StderrMessageDisplayer.class);
    }
}

La configuration réalisée signifie que si un type MessageDisplayer est rencontré, par défaut un StdoutMessageDisplayer sera injecté. Mais nous ajoutons deux règles dérogatoires qui examine la présence d'une annotation @Named("stdout") ou @Named("stderr") sur le site d'injection pour choisir l'un ou l'autre des MessageDisplayer. Par exemple si nous souhaitons utiliser StderrMessageDisplayer, nous pouvons écrire le constructeur de HelloPrinter ainsi :

public class HelloPrinter {
	// ...

	@Inject
	public HelloPrinter(@Named("stderr") MessageDisplayer displayer, Random random) {
		this.displayer = displayer;
		this.random = random;
	}

	// ...
}

Configurons maintenant l'injection du générateur pseudo-aléatoire Random. Pour cela, nous allons écrire dans le module une méthode chargée de créer l'objet Random avec une graine prédéfinie (pour générer de façon déterministe toujours les mêmes nombres à chaque exécution) :

public class HelloPrinterInjectionModule extends AbstractModule {
    // ...

    @Provides
    Random createRandom() {
        return new Random(1L);
    }

    // ...
}

Il nous reste maintenant à injecter le nom ainsi que le nombre minimal et maximal d'affichages. Nous pourrions utiliser un Record pour représenter ces paramètres :

public record HelloPrinterParams(String name, int displayMinTimes, int displayMaxTimes) {}

Nous pouvons instantier le module en passant ces paramètres :

public class HelloPrinterInjectionModule extends AbstractModule {
    private final HelloPrinterParams params;

    public HelloPrinterInjectionModule(HelloPrinterParams params) {
        super();
        this.params = params;
    }

    @Override
    protected void configure() {
        bind(MessageDisplayer.class).to(StdoutMessageDisplayer.class);
        bind(MessageDisplayer.class).annotatedWith(Names.named("stdout")).to(StdoutMessageDisplayer.class);
        bind(MessageDisplayer.class).annotatedWith(Names.named("stderr")).to(StderrMessageDisplayer.class);

       bind(String.class).annotatedWith(Names.named("name")).toInstance(params.name());
	   bind(int.class).annotatedWith(Names.named("displayMinTimes")).toInstance(params.displayMinTimes());
	   bind(int.class).annotatedWith(Names.named("displayMaxTimes")).toInstance(params.displayMaxTimes());
    }

    @Provides
    Random createRandom() {
        return new Random(1L);
    }
}

Nous devons également pour éviter toute ambiguïté pour l'injection, utiliser des annotations pour marquer les sites d'injection des paramètres name, minDisplayTimes et maxDisplayTimes, par exemple :

@Inject
public void setDisplayTimes(@Named("displayMinTimes") int displayMinTimes, @Named("displayMaxTimes") int displayMaxTimes) {
	this.displayMinTimes = displayMinTimes;
	this.displayMaxTimes = displayMaxTimes;
}

Il faut maintenant activer l'injecteur dans le main de notre programme avec le bon module de configuration (sachant que si notre programme est complexe, il est possible d'utiliser plusieurs modules d'injection) :

public class HelloPrinterMain {
    public static void main(String[] args) {
        var params = new HelloPrinterParams(
                args.length >= 1 ? args[0] : "foobar", // name
                args.length >= 3 ? Integer.parseInt(args[1]) : 0, // minDisplayTimes
                args.length >= 3 ? Integer.parseInt(args[2]) : 100 // maxDisplayTimes
        );
        var injector = Guice.createInjector(new HelloPrinterInjectionModule(params));
        var hp = injector.getInstance(HelloPrinter.class);
        hp.sayHello();
    }
}

⚠ Guice étant un injecteur dynamique, l'injection est réalisée à l'exécution. La dynamicité de l'injection apporte l'avantage que nous pouvons configurer le module selon des paramètres d'exécution (par exemple les arguments de args). L'inconvénient est qu'il est possible que notre configuration apportée par le module soit incomplète voire ambigúe si l'injecteur n'arrive pas à trouver une valeur pour un site d'injection. Dans ce cas une exception ConfigurationException est levée.

Quelques remarques :