ctrl-alt-Development
Your hotkey to alternative software development
Massa Productie
Er zijn natuurlijk vele wegen die naar een object leiden. En Service Locator is een hele goede. Maar Service Locator heeft wel de beperking dat je er alleen bestaande instances mee kan opzoeken. En niet nieuwe instances mee kan produceren. Verdorie. Wat nu als we (verspilziek dat we zijn :-) iedere keer een nieuw CoffeeDevice nodig zouden hebben? Hoor ik daar iemand 'Factory' roepen?
Een Factory is een object dat andere objecten maakt. In Taiwan
staat een grote fabriek die koffiezetapparaten maakt. Iets in die geest. Als design pattern heb je er twee verschillende smaken van. Het FactoryMethod pattern - dat object instantiëring delegeert aan een sub-klasse. En de AbstractFactory die een interface definieert waarmee je objecten kan maken.
Het Factory Method Pattern
Hieronder zie je het UML diagram van het FactoryMethod pattern. Wat op valt is dat CoffeeDrinker een abstracte klasse is geworden en een extra methode definieert :
createCoffeeProducer()
. Dit is een abstracte (niet geïmplementeerde) methode welke geïmplementeerd moet worden in een sub-klasse van CoffeeDrinker - de CoffeeDeviceDrinker of de EspressoDrinker welke de kennis hebben over welk koffiezetapparaat er gemaakt moet worden. Conceptueel misschien iets minder sterk, maar goed.
Het betekent ook dat alhoewel CoffeeDrinker onafhankelijk is, je altijd een sub-klasse nodig hebt als je echt wat wilt doen. De keus voor het soort coffee device wordt gemaakt in de sub klasse. Naast het feit dat je voor iedere nieuwe CoffeeProducer een subclass moet definiëren heb je nu ook afhankelijkheden tussen de CoffeeDrinker en zijn subclasses - wat nadelig kan zijn als je de implementatie van CoffeeDrinker wilt wijzigen. Al met al een beperkte oplossing voor ons probleem.
Het Abstract Factory Pattern
AbstractFactory gooit er nog een schepje boven op door een aparte interface te definiëren voor het aanmaken van CoffeeDevices. Dit is dus geen doe-het-zelf koffiezetapparaat maar een echte uit Taiwan! De CoffeeDrinker kent alleen de interfaces en krijgt zijn referentie naar de CoffeeProducerFactory via zijn constructor of via de Registry. (Hij maakt in ieder geval niet zelf een Factory aan - want dan heb je weer een statische referentie naar een klasse.)
Er zijn twee implementaties van CoffeeProducerFactory - een voor elk koffiezetapparaat. Dus het EspressoDevice wordt gemaakt door de EspressoDeviceFactory. Ze vormen een paar om het zo te zeggen - constructie en gebruik zijn van elkaar gescheiden door twee aparte interfaces.
Het toevoegen van een nieuw soort koffiezetapparaat (cappuccino!) kan eenvoudig gedaan worden door twee klassen toe te voegen (CappuccinoDevice en CappuccinoDeviceFactory (made in Italy)). Alle componenten zijn enkel afhankelijk van de interfaces. Een nadeel is wel het aantal klassen wat je nodig hebt om dit voor elkaar te krijgen.
Al dat gedoe met factories en de hoeveelheid extra klassen en interfaces die het oplevert doet je afvragen of dit nu niet anders kan, zonder jezelf te moeten verlagen tot Class.forName().
Nu zijn recentelijk de zogenaamde 'Inversion of Control/Dependency Injection' frameworks populair geworden (bijvoorbeeld de PicoContainer
en het Spring Framework
). Deze frameworks bieden de mogelijkheid om classes te instantiëren zonder direct te specificeren wat de dependencies zijn.
Dependency Injection
Dependency Injection is een mooie naam voor het instantiëren van een klasse en daarbij het (semi-)automatisch bepalen en toekennen van zijn afhankelijkheden. Het kan op een aantal verschillende manieren gedaan worden. De belangrijkste zijn : Construction Injection, Setter Injection en Interface Injection. De naam verklapt al een hoop over de werking dus ik zal hier alleen ingaan op Constructor Injection.
Als je een klasse wilt instantiëren moet je de constructor argumenten weten. Door gebruik te maken van reflectie (in de java.lang.reflect
package) kan je bepalen wat de argument
typen
zijn. Gegeven de argument typen kan je opzoeken welke instanties daarbij horen (bijvoorbeeld door gebruik te maken van een Registry).
Concreet in ons voorbeeld : Stel dat we op automagische wijze een de CoffeeDrinker willen instantiëren. De CoffeeDrinker neemt een CoffeeProducer type als argument voor zijn constructor. Als dit interface type geregistreerd is in de Registry dan kunnen we de implementatie er bij zoeken. Dan hebben we dus een waarde voor de constructor van CoffeeDrinker! Dan is het daarna slechts nog een kwestie van de constructor afvuren om een spik splinter nieuwe CoffeeDrinker te maken.
Afijn hieronder het proces in 3 stappen gevisualiseerd.
De vraag is nu nog even hoe doe je dit in (voorbeeld) code :
public class Factory {
public Object newInstance(Class type) {
//
Constructor[] cs=type.getConstructors();
for (int t=0;t<cs.length;t++) {
//
Class[] types=cs[t].getParameterTypes();
Object[] values=new Object[types.length];
//
if (resolve(types,values)) try {
//
return cs[t].newInstance(values);
//
} catch (Exception ex) {
throw new RuntimeException(
"Failed instantiating "+
type.getName(),
ex
);
}
}
//
return null;
}
Hierboven wordt via Reflectie de constructors van het gespecificeerde type opgevraagd en daarna wordt per constructor gekeken naar de benodigde argumenten. Wanneer van al deze argument types instanties beschikbaar zijn kan de constructor worden uitgevoerd en het resultaat terug gegeven worden. De resolve methode (onder) is verantwoordelijk voor het opzoeken van de argument waarden in de Registry en ze terug te geven in de value array.
protected boolean resolve(
Class[] types,
Object[] instances
) {
Registry r=Registry.getInstance();
for(int t=0;t<types.length;t++) {
instances[t]=r.get(types[t]);
if (instances[t]==null) return false;
}
return true;
}
}
Uiteraard is dit een heel eenvoudige implementatie die enkel Singleton objecten ondersteunt. Echter het is slechts een klein beetje meer werk om de link interface type -> implementatie type te leggen zodat ook het Factory pattern ondersteunt wordt waarbij het instantiëren wordt overgelaten aan de newInstance methode. Of dat de argumenten uit een een aparte configratie file worden gelezen. Of dat je gebruik maakt van JDK 1.5 annotaties. etc. etc. Maar goed! Genoeg gedroomd - Nu is het tijd voor een bak echte koffie :
public static void main(String[] args) {
//
Registry.getInstance().
register(
CoffeeProducer.class,
new CoffeeDevice()
);
//
Factory theFactory=new Factory();
//
CoffeeDrinker drinker=
(CoffeeDrinker)theFactory.newInstance(
CoffeeDrinker.class
);
//
drinker.drink();
//
}
Eerst registreren we de CoffeeProducer interface en zijn bijbehorende implementatie, het CoffeeDevice in de Registry. Dan bouwen we de klasse die de slimmigheid bevat en daarna roepen we deze aan. De methode newInstance loopt alle constructors af (er is er maar één) en kijkt of de argumenten van die constructor vervuld kunnen worden door de types op de zoeken in de Registry. Gelukkig bestaat er een instantie van de CoffeeProducer. Het resultaat : een compleet geconfigureerd CoffeeDrinker object.