ctrl-alt-Development
Your hotkey to alternative software development
Use the Constructor Luke!
Een alternatief is om de CoffeeProducer mee te geven als constructor argument :
public CoffeeDrinker(CoffeeProducer p) {
myCoffeeProducer=p;
}
en bij iedere instantie (instance) die je maakt van CoffeeDrinker een CoffeeProducer mee te geven :
//
CoffeeDrinker drinker=new CoffeeDrinker(
new CoffeeDevice()
);
//
drinker.drink();
//
Nou, de CoffeeDrinker is nu niet meer afhankelijk van het CoffeeDevice. Die verantwoordelijkheid ligt nu bij diegene die de CoffeeDrinker construeert. De verantwoordelijkheid is gedelegeerd. Een goede oplossing. Of niet?
Dat hangt er een beetje van af aan wie je het delegeert. Als die klasse inderdaad verantwoordelijk is voor de keuze voor het ene of de andere CoffeeProducer dan zou dat best eens kunnen. Doet de klasse echter niet meer dan het doorgeven van de CoffeeProducer dan is de keus twijfelachtig. Echter in beide gevallen is die klasse nogsteeds statisch gelinked. En dat wilden we toch voorkomen? Laten we een kijken of het Singleton pattern oplossing kan bieden.
Singleton to the Rescue?
Een Singleton
biedt toegang tot een enkele (gedeelde) instance van een type zonder te verklappen welk specifiek type dat is. We zouden bijvoorbeeld een CoffeeProducerSingleton kunnen maken welke de code bevat voor het aanmaken van de CoffeeProducer. En dan iedere keer als we de CoffeeProducer willen hebben hem opzoeken door middel van die Singleton. Zeg maar het Java equivalent van de Gouden Gids of Google.
Klinkt goed! Laten we eens kijken wat er gebeurt met de afhankelijkheden (dependencies) als we in ons voorbeeld de CoffeeProducer interface voorzien van een Singleton. Zo iets dus :
De implementatie van de CoffeeDrinkerSingleton kan als volgt zijn :
public class CoffeeProducerSingleton {
private static final
CoffeeProducer instance=new CoffeeDevice();
public static CoffeeProducer getInstance() {
return instance;
}
}
Met dit als resulterende code in de constructor van CoffeeDrinker :
public CoffeeDrinker() {
myCoffeeProducer=
CoffeeProducerSingleton.getInstance();
}
Wat gebeurt er : De CoffeeDrinker wil een CoffeeProducer hebben en vraagt dat aan de CoffeeProducerSingleton. Deze maakt een CoffeeDevice aan (als dat nog niet gebeurt was) en geeft het terug aan de CoffeeDrinker. Het lijkt Goed
tm
te werken, immers de CoffeeDrinker kent alleen de Interface en de CoffeeProducerSingleton. Echter het probleem wordt verplaatst. De CoffeeProducerSingleton kent op zijn beurt weer het CoffeeDevice. Dus indirect zitten ze toch nog steeds statisch aan elkaar gelinked echter nu via een tussen object.
Handige mensen roepen nu : maar waarom gebruik je geen
Class.forName
("CoffeeDevice").newInstance()
?? En inderdaad je kan heel prima via dynamic classloading, zonder direct naar het type te refereren, een instance maken. De vraag is wie je voor de gek houdt op deze manier? Java of Jezelf, aangezien er nog steeds een harde koppeling is ook al wordt deze tijdens het compileren niet meer opgemerkt. Bereid je maar voor op bergen ClassNotFoundExceptions
op volslagen onverwachte momenten.
Uiteraard kan je de CoffeeDrinker en de CoffeeProducerSingleton weer scheiden door een extra interface toe te voegen maar dan heb je het volgende probleem :
Hoe komt de CoffeeDrinker aan zijn CoffeeProducerSingleton?
En voor je het weet heb je een CoffeeProducerSingletonSingleton gemaakt... en zoef je eindeloos rond in cirkels.
Dit gaat dus niet werken. Op een één of andere manier moeten we dus voor elkaar zien te krijgen dat de Singleton wel het object kan terug geven maar niet de kennis heeft om het object te maken of probeert het aanmaken te delegeren. Mmmm daar heb ik eens wat over gelezen...
Service Locator
Service Locator is ook wel bekend onder de naam Registry. Eigenlijk een veralgemeende Singleton zonder kennis van specifieke typen. Met een Registry kan je een koppeling maken tussen een (interface) type en een object. Dat betekent dus dat de CoffeeDrinker tegen de Registry kan zeggen 'Welk object hoort bij de interface CoffeeProducer?' en krijgt dan netjes een object van het type CoffeeDevice of ExpressoDevice terug naar gelang welke koppeling er gemaakt is. In UML ziet het er als volgt uit :
Zoals je ziet weet de CoffeeDrinker alleen iets over de CoffeeProducer en de Registry. Perfect! En de Registry weet nog minder. Die weet alleen hoe een bepaalde klasse gekoppeld moet worden aan een bepaalde instance maar welke dat nu zijn boeit hem voor geen meter.
Waar de kennis dan wel zit? Nou dit is helemaal verschoven naar het initialisatie of start punt van het programma (de 'public static void main(String[] args)' zeg maar) Hier wordt de registry gevuld met de correcte interface/instance waarden. In het onderstaande voorbeeld link ik de CoffeeProducer interface aan een instance van CoffeeDevice.
public static void main(String[] args) {
//
// Set up
//
Registry.getInstance().register(
CoffeeProducer.class,new CoffeeDevice()
);
//
// Drink some Coffee.
//
new CoffeeDrinker().drink();
//
}
Een mogelijke implementatie van een Registry is iets in de geest van een HashMap gecombineerd met een Singleton :
public class Registry
private static Registry myInstance=new Registry();
public static Registry getInstance() {
return myInstance;
}
private Map myImpl=new HashMap();
public void register(
Class aType,
Object anImplementation
) {
myImpl.put(aType,anImplementation);
}
public Object get(Class aType) {
return myImpl.get(aType);
}
}
De constructor van CoffeeDrinker ziet er nu als volgt uit :
public CoffeeDrinker() {
myCoffeeProducer=(CoffeeProducer)
Registry.getInstance().get(
CoffeeProducer.class
);
}
Zoals je ziet lijkt deze constructor erg op het Singleton voorbeeld. De CoffeeProducer referentie wordt opgezocht door het werk uit te besteden aan een derde partij. Echter de derde partij heeft nu tijdens het compileren geen enkele referentie meer naar het CoffeeDevice. Deze kennis wordt pas bekend gemaakt zodra het programma wordt gestart - in de klasse waar de main methode zit (Main in het voorbeeld). En slechts deze klasse heeft de kennis van welke specifieke componenten er gebruikt worden. De rest van de applicatie werkt gescheiden van elkaar via interfaces.
Te gek! Dat is precies zoals we wilden!
Alhoewel het casten niet zo mooi is.. Maar er is vast wel een slimmerik die daar wat op bedenkt.