OBJECT SPAGHETTI : PATTERNS BIEDEN UITKOMST? Object georiënteerde (OO) systemen kennen vele voordelen ten opzichte van traditionele procedurele systemen. Zo zouden OO systemen flexibeler en beter onderhoudbaar zijn. In de praktijk valt dat nogal eens tegen. Een van de oorzaken is het niet of onvoldoende scheiden van de objecten - Object Spaghetti - Neem een object en de rest zit er aan vast. Naast hoe je het niet moet doen beschrijft en illustreert dit artikel de mogelijkheden die je als Java Programmeur hebt om deze ellende te omzeilen.
Wat is het probleem nou eigenlijk? Een object georiënteerd systeem bestaat uit klassen die elkaar diensten verlenen. Om elkaar diensten te kunnen verlenen moeten ze elkaar kennen (naar elkaar kunnen refereren) Bijvoorbeeld een object dat koffie drinkt (ik!) en een KoffieZetApparaat. Als ik koffie wil drinken heb ik een referentie nodig naar het KoffieZetApparaat en ben ik er dus van afhankelijk. Het betekent ook dat als je de KoffieDrinker wilt compileren je altijd de KoffieZetApparaat klasse nodig hebt. En wat nu als je een keer Espresso wilt? Dan zul je heel je programma moeten aanpassen en opnieuw compileren.
Leuk, zul je zeggen, maar daar hebben we in Java interfaces voor. Interfaces maken het mogelijk een onderscheid te maken tussen wat een object kan (zijn methoden) en zijn daadwerkelijke implementatie. Een interface kan dus worden geïmplementeerd door twee verschillende objecten die helemaal niets met elkaar te maken hebben, echter door de interface kunnen ze wel uniform benaderd worden. Laten we een interface CoffieProducer maken die werkt als scheiding tussen mij en het Koffie Zet Apparaat (CoffeeDevice) - zodat we de implementatie kunnen wijzigen zonder dat ik dingen moet leren over andere soorten KoffieZetApparaten. In
UML :
Helemaal goed! Behalve dan de vraag van hoe komt de CoffeeDrinker aan zijn CoffeeProducer? Dat is lastig want je kunt geen object maken van een interface - daar voor heb je een klasse nodig. En dat is de kern van het probleem - hoe komt een object dat gebruik maakt van een interface aan de implementatie van die interface ? Laten we eens wat proberen :
public class CoffeeDrinker { private CoffeeProducer myCoffeeProducer; public CoffeeDrinker() { myCoffeeProducer=new CoffeeDevice();
} public void drink() { System.out.println("Drinking "+myCoffeeProducer.getCoffee()); } } Of in UML :
In dit voorbeeld wordt de CoffeeProducer aangemaakt in de constructor van CoffeeDrinker. Deze oplossing werkt, maar uiteindelijk schiet je er niets mee op, want de CoffeeDrinker heeft nog steeds een harde (statische) afhankelijkheid met het CoffeeDevice. Dat wil zeggen als je CoffeeDrinker compileert heb je nog steeds de CoffeeDevice klasse nodig, terwijl de bedoeling nou net was om dat te voorkomen. Zucht..
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 Goedtm 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.
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
protected boolean resolve(Class[] types,Object[] instances) { Registry r=Registry.getInstance(); for(int t=0;t
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. Als we wat we gemaakt hebben nu in een UML vorm gieten dan ontstaat een interessant plaatje :
Er is nu scheiding ontstaan tussen applicatie specifieke klassen (de onderste helft), de framework klassen (Factory en Registry) en de Initialisatie (Main). Enkel de Main klasse heeft alle kennis in zich van de applicatie. De overige klassen zijn of onwetend of netjes gescheiden via interfaces. State of the art technology.. of niet?
Er is altijd een prijs te betalen voor het gebruik van frameworks die werken via reflectie. Ze zijn langzamer ten opzichte van echte implementaties. Daarnaast zijn ze ook een stuk ondoorzichtiger. Dat betekent als je aan het debuggen gaat en er wordt ergens een nieuw object gemaakt dat je eerst door een zooi reflectie code heen moet - dat is niet prettig.
Conclusie : Als we flexibele software willen schrijven dan is het verstandig om de onderdelen te scheiden door middel van interfaces. Echter we hebben gezien dat het gebruik van interfaces niet automatisch leidt tot flexibele software. Het feit dat je concreet een type moet kennen om er een object van te kunnen maken gooit roet in het eten. Er zijn drie strategieën die gebruikt kunnen worden afhankelijkheden te ontkoppelen :
•
Weten - waarin via overerving in de afhankelijkheid word voldaan. Het FactoryMethod pattern doet dit bijvoorbeeld. Uiteraard blijven de subklassen wel sterk verbonden met de originele ontkoppelde klasse, wat een nadeel kan zijn.
•
Opzoeken - waarin het object zelf opzoek gaat naar zijn afhankelijkheden. Met behulp van een Service Locator is dit mogelijk. Met deze methode houdt je altijd een enkel centraal (maar wel neutraal!) type over waarin de interfaces en implementaties samen komen.
•
Vertellen - het object wordt verteld wat zijn afhankelijkheden zijn door middel van zijn constructor en/of een setter methods. Door middel van Dependency Injectie kan dit nagenoeg transparant zonder eindeloos parameters te hoeven doorgeven van de ene naar de andere constructor. Een alternatief vormt de AbstractFactory waarbij het aanmaken van het object wordt geabstraheerd achter een interface (waar je natuurlijk weer een implementatie van moet zien te krijgen)
De prijs die voor het ontkoppelen betaalt moet worden is afhankelijk van de gekozen technologie. Bij het gebruik van het AbstractFactory pattern zegt de (vele) code duidelijk wat het doet. Bij Dependency Injection gebeurt een hoop onderwater via reflectie - ondoorzichtig - echter het is wel een manier waarbij je veel code kunt vervangen door initialisatie en configuratie.
Erik Hooijmeijer
Ctrl-Alt-Dev Erik Hooijmeijer is senior developer bij 42 B.V. Naast het ontwerpen en bouwen van bedrijfs kritische toepassingen in Java is hij altijd bezig met de ontwikkeling van zogenaamde speelbare concepten.