service container vraag
Ik maak gebruik van een service container, maar ik vraag me iets af. Stel ik heb in class Foo een class Bar nodig, dan geef ik deze class Bar als argument mee aan Foo. Dit stel ik in in het configuratiebestand en de container regelt dan de rest. Echter, het kan ook voorkomen dat je in een bepaalde class niet weet óf je een bepaalde class nodig hebt, en zo ja welke. Stel je hebt bijvoorbeeld deze functie...
Code (php)
Hoe doen we dit nu met een service container? We mogen in een class niet hardcoded classes declareren. Maar hoe kan ik dan toch de juiste objecten aanmaken? Is het dan de bedoeling dat ik de complete service container meegeef aan de betreffende class en dat je dan zoiets als dit krijgt? (zelfde functie maar dan met services)
Code (php)
Is dat de jusite manier? Graag jullie advies!
Ozzie PHP op 01/04/2013 14:23:52:
Mede daarvoor heb je dus een autoloader :)Echter, het kan ook voorkomen dat je in een bepaalde class niet weet óf je een bepaalde class nodig hebt, en zo ja welke.
Als je een service container gebruikt is het de bedoeling dat je in geen enkele class meer hardcoded een andere class aanroept. Dus in plaats van voorbeeld 1 gebruik je dan voorbeeld 2. Alleen de vraag is of ik dan de complete service container moet meegeven aan de betreffende class.
Quote:
is het de bedoeling dat je in geen enkele class meer hardcoded een andere class aanroept.
Oh?
Quote:
Alleen de vraag is of ik dan de complete service container moet meegeven aan de betreffende class.
Nee, dat nooit. Dat heeft de naam 'Service Locator' gekregen, een echt anitpattern.
Wouter J op 01/04/2013 14:58:52:
Oh?
Quote:
is het de bedoeling dat je in geen enkele class meer hardcoded een andere class aanroept.
Oh?
Zo heeft Erwin het uitgelegd. En ik vind dat wel heel erg mooi eigenlijk. Alles wordt dan vanuit je configuratie geregeld. Een class kent dan zelf geen verantwoordelijkheden meer.
Wouter J op 01/04/2013 14:58:52:
Nee, dat nooit. Dat heeft de naam 'Service Locator' gekregen, een echt anitpattern.
Hmm, oké. Maar in de tutorial hier op PHPhulp wordt dat wel gedaan toch? Dan wordt de complete container meegegeven volgens mij.
Anyhow... enig idee hoe ik dat dan moet oplossen als ik vantevoren niet weet of in een bepaalde class service A of service B gebruikt moet worden? Hoe zorg ik dat ik die dan toch kan aanroepen?
Er is een groot verschil tussen "need to know" enerzijds en "total control" anderzijds. Je hinkt daarom ergens op twee gedachten. Enerzijds wil je een objectmodel waarin het ene object niets afweet van het bestaan van andere objecten. Prima, want daarvoor hebben we een SPL-autoloader. Anderzijds wil je via één centraal configuratiebestand kunnen regelen welke objecten *moeten* worden geladen. Daarvoor gebruik je de servicecontainer als objectmanager.
Stel we hebben een class die in het ene geval de standaard database moet gebruiken, maar in test-mode de test-database.
In het configuratiebestand voor de service container zou je nu 2 database services moeten aanmaken. Een 'database' en een 'database-test'. De database krijgt automatisch in zijn constructor de inloggegevens van de standaard database geinjecteerd. De test database krijgt automatisch de inloggegevens van de test database geinjecteerd. We gebruiken services zodat we automatisch de juiste inloggegevens kunnen meegeven vanuit het configuratiebestand, maar ook omdat we naderhand makkelijk de inloggegevens kunnen wijzigen of een andere database class kunnen gebruiken zonder dat we de code hoeven aan te passen. We hoeven uitsluitend het configuratiebestand aan te passen.
Stel nu, we hebben dus een class User. Normaal gesproken gebruikt deze de standaard database om een user op te halen, maar in de testmode moet de test database worden gebruikt.
Stel dat alleen de standaard database zou moeten worden gebruikt, dan zou je deze in het configuratiebestand meegeven als argument in de constructor. Echter, in dit geval weet je niet of de standaard database moet worden gebruikt, of de test database. De User class moet dus in staat zijn om beide services aan te kunnen roepen. Volgens mij kan dat alleen maar als je de service container meegeeft aan de User class. Maar Wouter zegt dat dit niet goed is. Maar hoe zou je het dan moeten oplossen?
je kan bijv. iets doen als:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
// ...
$container->set('database_prod', function ($c) {
return new Database($c->get('database.user'), $c->get('database.pass'), ...);
});
$container->set('database_test', function ($c) {
return new Database($c->get('database.test.user'), $c->get('database.test.pass'), ...);
});
$container->set('usermapper', function ($c) {
return new UserMapper($c->get('database'));
});
if ('test' === $container->getParameter('app.environment')) {
$container->setAlias('database', 'database_test');
} else {
$container->setAlias('database', 'database_prod');
}
// ...
$container->set('database_prod', function ($c) {
return new Database($c->get('database.user'), $c->get('database.pass'), ...);
});
$container->set('database_test', function ($c) {
return new Database($c->get('database.test.user'), $c->get('database.test.pass'), ...);
});
$container->set('usermapper', function ($c) {
return new UserMapper($c->get('database'));
});
if ('test' === $container->getParameter('app.environment')) {
$container->setAlias('database', 'database_test');
} else {
$container->setAlias('database', 'database_prod');
}
Je kunt zelfs verschillende autoloaders voor verschillende subsystemen combineren en ze met andere prioriteit registreren via de SPL. Dat zou dus aansluiten op jouw voorbeeld: een testomgeving laadt voor tests ontwikkelde/aangepaste klassen met een hogere prioriteit.
Volgens mij denk je te veel vanuit controle. Tegen een class User zeg je: "Je moet deze database gebruiken als service." Zo introduceer je afhankelijkheden, want bij een groeiend platform krijg je tientallen en honderden keren zo'n "je moet".
Plan B is dan door tussenkomst van De Heilige Configuratie alles regelen. Zolang alle klassen het Hoogste Woord van de Heilige Configuratie maar eerbiedigen, komt het "vanzelf" goed. De ene afhankelijkheid wordt zo verruild voor een andere: het hele kaartenhuis is afhankelijk van één configuratiebestand.
Ik zou dat liever omkeren. Een class User heeft een database nodig en vraagt: "Waar kan ik een databaseservice krijgen?"
Ik had een class gemaakt waarin ik de sessie ($_SESSION) gegevens wilde opslaan. Echter, er moeten dan wel sessie gegevens aanwezig zijn. Als er geen sessie is gestart, zijn er ook geen sessie gegevens. Nu wilde ik in de request class controleren (op basis van de cookie 'PHPSESSID') of er een sessie is gestart. Zo ja, dan zou die sessie class moeten worden aangemaakt. Echter, zo niet... dan moest die class niet worden aangemaakt. Het probleem is dus dat de class soms wel en soms niet moet worden aangemaakt. Daarom kan je hem niet aa de constructor meegeven want dan is ie al aangemaakt. In dit specifieke geval kun je volgens mij alleen de container meegeven aan de request class. De request class kijkt of de sessie class moet worden aangemaakt. Zo ja, dan plukt ie die uit de container. Of is er een andere manier?
@Ward:
Ja, de autoloader bepaalt welke class er wordt geladen, maar dat is niet wat ik bedoel. Of ik begrijp je niet goed.
Stel we hebben een class 'Auto' en die class heeft een class 'Motor' nodig.
Via de constructor van de class 'Auto' geef ik een 'Motor' object mee.
Code (php)
Vanuit het configuratiebestand kan ik nu bepalen welke class 'motor' is. De echte class is bijvoorbeeld 'Motor_Version_Three_Core'.
Die 'Auto' class heeft geen idee welke class hij gebruikt. Het enige wat ie weet is dat het om een 'motor' class gaat. Moet de motor class vervangen worden door een versie 4, dan hoef ik alleen het configuratiebestand aan te passen.
Gewijzigd op 01/04/2013 16:09:30 door Ozzie PHP
De service container (of factory) bepaalt welke class geladen moet worden op basis van parameters die het krijgt. In de service container zal dus bepaald moeten worden of die test database configuratie geladen moet worden, of dat de echte database configuratie geladen moet worden.
De user class vervolgens geeft alleen de service container een schop om te zeggen dat hij zo'n database ding wil hebben. Zal hem worst wezen welke.
Het voorbeeld (eerste post) is dus ook niet goed. In die class moet je helemaal niet zo'n if structuur hebben. Op die manier kan je net zo goed direct de class inladen op dat punt, want de afhankelijkheid is er op dat moment toch al.
Thanks voor je toelichting Erwin. Maar wat nu als je uberhaupt niet weet of een class wel of niet geladen moet worden (zie voorbeeld hierboven).
Ozzie, dat is precies hetzelfde. Ook een autoloader kan bepalen wat er gebeurt bij een new Auto() of een new Motor(). De autoloader bepaalt namelijk wat er wordt geladen...
De Service Container is er maar voor 1 ding: Bepalen welke klassen een andere klasse krijgt.
Wouter J op 01/04/2013 16:21:04:
Precies, en dus bepalen wat je hebt geüpload naar /Auto/ en /Auto/Motor/ via de autoloader welke klassen worden geladen, niet dat we 'Motor_Version_Three_Core' via een oonfig-bestand gebruiken als new Motor().De autoloader is er maar voor 1 ding: Klassen proberen te laden die nog niet bestaan.
De Service Container is er maar voor 1 ding: Bepalen welke klassen een andere klasse krijgt.
De Service Container is er maar voor 1 ding: Bepalen welke klassen een andere klasse krijgt.
Daar komt de verwarring, volgens mij, ook vandaan: de service container wordt misbruikt voor version control.
Ozzie PHP op 01/04/2013 16:17:13:
Thanks voor je toelichting Erwin. Maar wat nu als je uberhaupt niet weet of een class wel of niet geladen moet worden (zie voorbeeld hierboven).
Als je niet weet welke class geladen moet worden, dan gebeurt er helemaal niets.... dat bedoel je dus niet.
1) Een object in je systeem wil een ander object hebben. Hij weet niets van andere classes, dus kan ook niet bepalen welke hij moet hebben. Hij weet alleen welke kenmerken dat object moet hebben. Die kenmerken geeft hij door aan de service container (of factory).
2) service container bekijkt die kenmerken en loopt zijn lijstje of hij een class op in zijn systeem heeft staan dat er aan voldoet. Als hij er een vind dan gaat hij die laden door de naam door te geven aan de autoloader.
3) de autoloader krijgt de naam en loopt zijn magazijn af. Aangekomen bij de juiste class laadt hij die en geeft hem aan het systeem.
4) service container geeft het object terug aan de aanvrager
5) aanvrager gaat weer lekker door met zijn werk, zonder overigens te weten hoe zijn nieuwe speeltje heet. Hij weet alleen dat dat speeltje kan doen wat het moet doen.
Aanvrager weet niets van welke classes beschikbaar zijn, alleen wat hij nodig heeft.
Service container weet niet waarom het nodig is, maar weet wel wat hij beschikbaar heeft.
Autoloader weet niet welke classes er allemaal zijn, maar kan als er een wordt aangevraagd die vinden.
Toevoeging op 01/04/2013 16:36:21:
Ward van der Put op 01/04/2013 16:30:00:
Daar komt de verwarring, volgens mij, ook vandaan: de service container wordt misbruikt voor version control.
Nee, niet de service container, het config bestand. Precies waar het voor bedoeld is. In elk geval moet de autoloader ook niet als version controller gaan opereren, die moet gewoon de boel laden wanneer het nodig is (alles meer is eigenlijk oneigenlijk gebruik, omdat de autoloader sowieso al buiten de OOP structuur valt, het is een simpele functie, geen class).
Ward, een autoloader laadt alleen een class in die nog niet geladen is. Niks meer en niks minder. Een autoloader is ervoor bedoeld dat jij niet telkens handmatig een bestand moet requiren voordat je een class gaat gebruiken. Dus je hoeft niet dit te doen:
maar simpelweg dit:
De class wordt nu gerequired door de autoloader. De autoloader bepaalt echter niet WELKE class er geladen wordt. Dat doe jij zelf door te zeggen $class = new Class();
Nu weer terug naar het verhaal van Erwin.
Stel we hebben een class 'Auto' en die class heeft een 'motor' class nodig. Hoe ik het nu zou doen, is dat ik in het configuratiebestand een service 'Auto' aanmaak. Dit ziet er (versimpeld) als volgt uit:
The constructor van de class auto ziet er als volgt uit:
De constructor van de Auto class heeft dus geen idee welke class de Motor class is. En als ik ook een andere Motor class wil injecteren, dan pas ik dus alleen het config bestand aan.
Klopt dat zo?
Gewijzigd op 01/04/2013 17:19:58 door Ozzie PHP
De vraag is nu of via de arguments al volledig vaststaat welke motor nodig is, of dat dat alleen een kenmerk van de motor moet zijn? Kan de service container op basis hiervan bepalen het is een motor met 16 kleppen, maar kan een 1.2 liter, een 1.4 liter of een 2.0 liter zijn? Of is dit in feite al de naam van de class? Beide gevallen kunnen overigens juist zijn, ligt er maar net aan wat de mogelijkheden zijn en hoeveel flexibiliteit er nog nodig is.
Ik heb in een class een andere class nodig... en ik geef die class dan mee via de service container. Ik bepaal zelf welke class ik meegeef (door de naam van de class). Ik zou eerlijk gezegd niet weten wat ik voor extra intelligentie zou moeten inbouwen, en hoe...
Toevoeging op 01/04/2013 17:42:15:
Maar stel nu... ff heel stom voorbeeldje he, maar t gaat even om het idee ;)
Stel nu... er zijn geen schroeven aanwezig om de motor vast te zetten. Dan hebben we dus ook geen motor nodig, en had er dus ook geen motor object hoeven te worden aangemaakt.
Hoe kun je bovengenoemde situatie voorkomen? Stel dat je pas in de Auto class zelf weet of er schroeven zijn? Zo ja, dan heb je de motor nodig, zo nee dan heb je de motor niet nodig. Echter, omdat je de motor meegeeft in de constructor heb je altijd een motor, ongeacht of er schroeven zijn. Kun je in zo'n geval voorkomen dat het motor object wordt aangemaakt?
Ozzie PHP op 01/04/2013 17:18:52:
Ward, een autoloader laadt alleen een class in die nog niet geladen is. Niks meer en niks minder. Een autoloader is ervoor bedoeld dat jij niet telkens handmatig een bestand moet requiren voordat je een class gaat gebruiken. Dus je hoeft niet dit te doen:
maar simpelweg dit:
De class wordt nu gerequired door de autoloader. De autoloader bepaalt echter niet WELKE class er geladen wordt. Dat doe jij zelf door te zeggen $class = new Class();
maar simpelweg dit:
De class wordt nu gerequired door de autoloader. De autoloader bepaalt echter niet WELKE class er geladen wordt. Dat doe jij zelf door te zeggen $class = new Class();
Ja en nee. Ik sleutel nu bijvoorbeeld, met andere ontwikkelaars, aan een nieuwe SEPA-compatibele module voor iDEAL. In de testomgeving hangt die aan elkaar met plakband en elastiek. Maar in de productieomgeving wordt het uiteindelijk een kwestie van bestandjes vervangen in de juiste directory.
De autoloader "weet" in dat opzicht dus inderdaad niet wat er wordt geladen. Maar het voortraject is essentieel: in een /Payments/iDEAL/ staat de laatste stabiele versie van het ding.
Daarom is dit, volgens mij, een andere benadering dan je in een test- of ontwikkelomgeving zou volgen. Dan kun je inderdaad "ik wil bètaversie 0.95 aan de tand voelen" omleiden via een objectmanager. In een productieomgeving wil je dat niet.
Ik zou in dat grotere dus óók geen service controler willen zien die een afwijkende beslissing mogelijk maakt: "Gebruik de vorige versie van de iDEAL-module." En al helemaal niet: "Gebruik de bètaversie van de iDEAL-module."
Als er een betere en veiligere versie van een klasse is, zou díé de baas moeten zijn. Daarom vind ik het fundamenteel verkeerd om een service container op te voeren die kan zeggen: "Schijt aan alle verbeteringen en beveiligingen; ik laad doodleuk de versie die in het laatste configuratiebestand staat."
Ward van der Put op 01/04/2013 17:44:37:
Als er een betere en veiligere versie van een klasse is, zou díé de baas moeten zijn. Daarom vind ik het fundamenteel verkeerd om een service container op te voeren die kan zeggen: "Schijt aan alle verbeteringen en beveiligingen; ik laad doodleuk de versie die in het laatste configuratiebestand staat."
Tja, dat is dan denk ik een verschil in werkwijze. Mijn service container is bedoeld om te kunnen zeggen, gebruik in class Auto als motor de class ...
Op het moment dat ik een betere motor class heb gemaakt, dan plaats ik die in het configuratiebestand (IK bepaal dus welke motor wordt gebruikt). Ik laat niet het versienummer of de bestandslocatie bepalend zijn. Maar dat is een kwestie van keuze. In mijn opzet is het configuratiebestand bepalend, in jouw opzet de bestandslocatie.