Même si l'on peut déclarer des variables d'un type abstrait, les constructeurs font nécessairement partie d'un type concret. Une application qui contient un grand nombre d'appels à des constructeurs est donc très dépendante des classes concrètes choisies. Si l'on décide de changer l'implémentation de certains types abstraits, il faut remplacer toutes les invocations des constructeurs des classes concrètes correspondantes, ce qui n'est pas commode et sujet à erreurs. Il est préférable de rassembler tous les choix d'implémentation dans un objet particulier, appelé fabrique, auquel sera déléguée la construction des objets. Cette technique constitue le pattern de fabrication.
Prenons l'exemple d'une application qui doit utiliser des listes, des ensembles et des tables. Les variables et les paramètres des méthodes seront déclarées à l'aide des interfaces List, Set et Map.
La construction de listes, d'ensembles et de tables est déléguée à une fabrique f, déclarée d'un type abstrait Fabrique. La fabrique est créée à l'aide d'un constructeur d'une classe concrète qui rassemble les choix d'implémentations. Ici la classe MaFabrique indique que les implémentations ArrayList, HashSet et HashMap sont choisies. Dans la suite du programme, là où l'on aurait invoqué un constructeur, new ArrayList(), new HashSet() ou new HashMap(), on invoque une méthode déclarée dans le type abstrait Fabrique sur l'objet fabrique : respectivement f.fabriquerList(), f.fabriquerSet() ou f.fabriquerMap() :
Fabrique f = new MaFabrique(); List l = f.fabriquerList(); Set s = f.fabriquerSet(); Map m = f.fabriquerMap();
Fabrique est une interface, et c'est en l'implémentant qu'on décide quelles implémentations vont être utilisées :
interface Fabrique { List fabriquerList(); Set fabriquerSet(); Map fabriquerMap(); } class MaFabrique implements Fabrique { List fabriquerList(){ return new ArrayList(); } Set fabriquerSet(){ return new HashSet(); } Map fabriquerMap(){ return new HashMap(); } }
Si d'autres choix sont à faire, il suffit de modifier la classe concrète MaFabrique ; mieux, on définit une autre classe concrète implémentant Fabrique et on modifie la seule ligne Fabrique f = new MaFabrique(). Par exemple, si l'on préfère, pour des raisons de préservation de l'ordre des données, des implémentations des ensembles et des tables par des arbres plutôt que par hachage, on définira :
class FabriqueParArbres implements Fabrique { List fabriquerList(){ return new ArrayList(); } Set fabriquerSet(){ return new TreeSet(); } Map fabriquerMap(){ return new TreeMap(); } }
Il suffira de définir une fabrique par :
Fabrique f = new FabriqueParArbres();
Le reste du programme n'a pas besoin d'être modifié. Dans une approche plus systématique, l'application figurerait dans une classe Application, dont un constructeur aurait un paramètre de type Fabrique. L'instanciation de l'application se ferait par :
... new Application(new MaFabrique()) ...