[oop] valid state
Ik probeer voor mezelf wat duidelijker te krijgen wat precies een "valid state" van een object is. Regelmatig lees ik dat je via de constructor een object in een valid state moet brengen. Nu vraag ik me af hoe ik dat moet zien.
Een fictief voorbeeldje. Stel ik heb een class die een pannenkoek bakt :-)
Nu geef ik de ingrediënten voor de pannenkoek mee aan de constructor.
Ik heb nu dus de ingrediënten aan de constructor doorgegeven en deze worden geset als class property. Is de class nu in een "valid state"?
Als ik de de pannenkoek uit de pannenkoekenbakker wil halen, ziet dat er zo uit:
Code (php)
1
2
3
4
2
3
4
<?php
$pannenkoekenbakker = new Pannenkoekenbakker($ingredienten);
$heerlijke_pannenkoek = $pannenkoekenbakker->getPannenkoek();
?>
$pannenkoekenbakker = new Pannenkoekenbakker($ingredienten);
$heerlijke_pannenkoek = $pannenkoekenbakker->getPannenkoek();
?>
De oplettende lezer zal het misschien zijn opgevallen dat ik een pannenkoek opvraag, maar dat er nog geen pannenkoek is gebakken!
Mijn vraag is nu, was de Pannenkoekenbakker class hierboven dan wel in een "valid state"? Had de constructor eerst een pannenkoek moeten bakken om een "valid state" te bereiken? Of is het voldoende om in de constructor uitsluitend de ingrediënten te setten om in een "valid state" te geraken, en moet ik vervolgens zelf handmatig de "bakken" method aanroepen voordat ik een pannenkoek kan opvragen? Zo dus:
Code (php)
1
2
3
4
5
2
3
4
5
<?php
$pannenkoekenbakker = new Pannenkoekenbakker($ingredienten);
$pannenkoekenbakker->bakken();
$heerlijke_pannenkoek = $pannenkoekenbakker->getPannenkoek();
?>
$pannenkoekenbakker = new Pannenkoekenbakker($ingredienten);
$pannenkoekenbakker->bakken();
$heerlijke_pannenkoek = $pannenkoekenbakker->getPannenkoek();
?>
Wanneer is de "valid state" bereikt vraag ik me af? Als ik in dit laatste voorbeeld een pannenkoek opvraag, terwijl die nog niet is gebakken dan heb ik een probleem. Is het dan toch de taak van de constructor om de pannenkoek te bakken om zodoende een "valid state" te bereiken?
Gewijzigd op 27/05/2014 23:28:02 door Ozzie PHP
De class Pannenkoekenbakker kan bijvoorbeeld bij de aanroep van bakken() ook terugmelden: "Pannenkoeken bakken is op dit moment niet mogelijk, want mijn recources (ingrediënten) zijn op."
Het verhaal verandert als een class Pannenkoekenbakker altijd een object moet opleveren dat kan bakken(). Dan zul je dat in de constructor moeten regelen, zodat na new Pannenkoekenbakker() direct bakken() kan worden aangeroepen.
Je kunt het vergelijken met de constructors van PDO en MySQLi. Kan er geen databaseverbinding worden geopend, dan falen die en krijg je geen "valid" object. Logisch, want je hebt zonder connectie geen "valid" connectie-object: het object is een representatie van iets anders, maar dat bestaat niet, dus is het object "invalid". Dat kan, net zoals bij je Pannenkoekenbakker, gebeuren doordat je ze de verkeerde ingrediënten meegeeft of doordat er te weinig externe resources zijn (bijvoorbeeld door een "Too many connections").
Ja, de class wel. Over het object kan ik niets zeggen. Maar als de constructor correct (case-by-case basis) geschreven is, dan wel. Er vanuit gaand dat er geen exception gegooid is natuurlijk, in dat gevan is er namelik geen object. (Er vanuit gaand dat er geen 'this leak' plaats vindt in de constructor.)
"De oplettende lezer zal het misschien zijn opgevallen dat ik een pannenkoek opvraag, maar dat er nog geen pannenkoek is gebakken!"
Dit klinkt eerder als een method die hernoemt moet worden zodat het duidelijk wordt dat er telkens een nieuwe pannenkoek gebakken wordt.
Als je eerst bakken() aan moet roepen voordat je getPannenkoek() successvol aan kan roepen zou het zo kunnen zijn dat je de fout hebt gemaakt om het Sequential coupling anti-pattern toe te passen. (Weer een case-by-case basis, een legitieme use case is dat de remove($key) methode alleen een non-null waarde returned als er eerst add($key, $value) is aangeroepen met de zelfde key.)
Een reden om zulke constructies te vermijden is dat het bij multi-threading niet echt lekker werkt. Dingen afleren is moeilijk, begin zo vroeg mogelijk.
En wat als we nu getPannenkoeken hernoemen naar bakken? Klaar!
@Wouter
>> En wat als we nu getPannenkoeken hernoemen naar bakken? Klaar!
Dat zou kunnen, maar ik wil direct als ik getPannenkoek aanroep een pannenkoek krijgen, en ik wil niet dat ie dan nog moet worden gebakken. Ik wil dus dat dat pannenkoek al klaar ligt op het moment dat ik hem aanroep.
@Dos:
>> Ja, de class wel. Over het object kan ik niets zeggen.
Oké... goede opmerking. En daar heb ik gelijk een vraag over. Is het de taak van de constructor om een class in valid state te brengen, of een object?
>> Als je eerst bakken() aan moet roepen voordat je getPannenkoek() successvol aan kan roepen zou het zo kunnen zijn dat je de fout hebt gemaakt om het Sequential coupling anti-pattern toe te passen.
Dat is precies wat er aan de hand is! Ik moet eerst bakken alvorens ik mijn pannenkoek kan opvragen! Fijn om te leren dat dit dus een anti-pattern is wat ik niet moet gebruiken in dit geval. Is de oplossing dan inderdaad om in de constructor al te gaan bakken?
@Ward:
>> ...zodat na new Pannenkoekenbakker() direct bakken() kan worden aangeroepen.
Bedoel je dan dat bakken() IN de contructor zelf wordt aangeroepen?
Dat is behoorlijk krom toch? Of hij moet worden gebakken of op zijn minst worden opgewarmt. Dus een bakken() en opwarmen() method zou dan beter zijn.
Vertel eens je echte probleem, dan kunnen we daar feedback op geven.
Quote:
Is de oplossing dan inderdaad om in de constructor al te gaan bakken?
Geen idee. Dat zou betekenen dat één Pannenkoekenbakker object maar één pannenkoek kan bakken. En ik kreeg nou juist het idee dat Pannenkoekenbakker een factory was zodat je niet telkens een nieuw Pannenkoekenbakker object hoeft je maken voor pannenkoeken met de zelfde ingredienten. Dan zou de callee namelijk net zo goed zelf een Pannenkoek object aan kunnen maken.
Quote:
Is het de taak van de constructor om een class in valid state te brengen, of een object?
De constructor is (in PHP) een (speciale) instance method. Voor elk object wordt een constructor aangeroepen. Het kan dus niet zo zijn dat constructors de class in een valide staat moeten brengen aangezien het mogelijk is dat een een class wordt gebruikt zonder dat er een instantie van die class bestaat. Dat zou betekenen dat de constructor niet is uitgevoerd en dus de class zich in een invalide staat bevind. Dit mag niet voorkomen.
In Java zou je kunnen zeggen dat een constructor een speciaal non-static initilizer block is, welke constructor uitgevoerd wordt hangt af van de parameters die je mee geeft, je kan die daarna niet nog eens uitvoeren.
Een static initializer block is er logischerwijs verantwoordelijk voor om de class in een valide staat te brengen.
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package x;
import y;
import z;
public class Test {
// non-static initializer
{
System.out.println("wordt uitgevoerd tijdens het creren van een object, ik geloof n Object's constructor en vr alle andere parent constructors");
}
// static initialize block
static {
System.out.println("wordt uitgevoerd tijdens het laden van de class");
}
// conditioneel non-static initializer
public Test(String fullName) {
// ...
}
// conditioneel non-static initializer
public Test(String firstName, String LastName) {
// ...
}
}
import y;
import z;
public class Test {
// non-static initializer
{
System.out.println("wordt uitgevoerd tijdens het creren van een object, ik geloof n Object's constructor en vr alle andere parent constructors");
}
// static initialize block
static {
System.out.println("wordt uitgevoerd tijdens het laden van de class");
}
// conditioneel non-static initializer
public Test(String fullName) {
// ...
}
// conditioneel non-static initializer
public Test(String firstName, String LastName) {
// ...
}
}
Als we dat vertalen naar PHP krijgen we zoiets als dit:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
namespace x;
use y;
use z;
// static initializer block A {
echo "wordt uitgevoerd tijdens het laden van de class";
// }
class Test {
// geen non-static initializer blocks mogelijk
public function __construct($a, $b = NULL) {
// ...
}
}
// static initializer block B {
echo "wordt uitgevoerd tijdens het laden van de class";
// }
?>
namespace x;
use y;
use z;
// static initializer block A {
echo "wordt uitgevoerd tijdens het laden van de class";
// }
class Test {
// geen non-static initializer blocks mogelijk
public function __construct($a, $b = NULL) {
// ...
}
}
// static initializer block B {
echo "wordt uitgevoerd tijdens het laden van de class";
// }
?>
Gewijzigd op 28/05/2014 11:39:33 door Dos Moonen
Ozzie PHP op 28/05/2014 10:59:23:
@Ward:
>> ...zodat na new Pannenkoekenbakker() direct bakken() kan worden aangeroepen.
Bedoel je dan dat bakken() IN de contructor zelf wordt aangeroepen?
>> ...zodat na new Pannenkoekenbakker() direct bakken() kan worden aangeroepen.
Bedoel je dan dat bakken() IN de contructor zelf wordt aangeroepen?
Nee, als een new Pannenkoekenbakker() alleen een geldige Pannenkoekenbakker is indien deze de methode bakken() ondersteunt maar het object daarvoor eerst in een bepaalde toestand moet worden gebracht, dan hoor je daarvoor de constructor te gebruiken.
Je moet voorkomen dat je objecten moet initialiseren. Dat is het domein van de constructor. Of anders gezegd: je wilt niet dat er ongeldige objecten in omloop zijn, want daarmee programmeer je bugs die later tot problemen kunnen leiden. Zoals het Wikipedia-artikel over sequential coupling ook zegt: de aanwezigheid van een methode met "init", "start" of "begin" is een veeg teken dat je een anti-pattern gebruikt.
Wil je niet de constructor gebruiken, dan kun je een ander creational pattern gebruiken om de verantwoordelijkheid te delegeren aan een andere klasse, bijvoorbeeld aan een PannenkoekenbakkerInstituut. Dat PannenkoekenbakkerInstituut zorgt er vervolgens voor dat er alleen Pannenkoekenbakkers te werk worden gesteld die de methode bakken() ondersteunen.
@Dos: oké. Dank je voor de toelichting. Een constructor brengt dus een object in een valide staat.
Ik zal nog even op de vraag van Wouter in gaan.
>> Vertel eens je echte probleem, dan kunnen we daar feedback op geven.
Het was een gedachte waar ik mee speelde. Lastig om uit te leggen. Het had te maken met als je iets wilt gebruiken wat eerst geconfigureerd moet worden. Maar beter zou je dan het Factory pattern kunnen gebruiken denk ik nu. Stel je wilt bijv. services configureren, ik zat te denken... moet je dan zoiets doen:
Code (php)
1
2
3
4
5
2
3
4
5
<?php
$configurator = new ServicesConfigurator($input);
$configurator->configure(); // hiet zat dus mijn twijfel, moet de constructor dit niet doen?
$services = $configurator->getServices();
?>
$configurator = new ServicesConfigurator($input);
$configurator->configure(); // hiet zat dus mijn twijfel, moet de constructor dit niet doen?
$services = $configurator->getServices();
?>
Nu ik er even met jullie over heb gebabbeld denk ik dat dit een beter oplossing is:
Gewijzigd op 28/05/2014 13:07:01 door Ozzie PHP
Factory pattern zozo
Zo heeft dat toch? Of zeg ik iets raars nu?
Ipv nog een klasse erbij te maken omdat we zo graag een aparte configure method willen hebben moeten we eerder gaan nadenken of dit wel is wat je wilt.
Waarom zou ServiceConfigurator bijv. niet met meerdere inputs kunnen werken? Waarom zou ik per input een nieuwe instance moeten aanmaken? Het zou veel logischer zijn als ServiceConfigurator een stateless class zou zijn en de configure method de services in de container zou zetten aan de hand van de input:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$loader = new ContainerChainLoader(array(
$yamlLoader, $xmlLoader, $phpLoader
));
$resources = array(
BASE_DIR.'/config/services.yml',
// ...
);
foreach ($modules as $module) {
$resources[] = $module->getConfigResources();
}
$containerConfig = new SeriveConfigurator($loader);
$containerConfig->configure($container, $resources);
?>
$loader = new ContainerChainLoader(array(
$yamlLoader, $xmlLoader, $phpLoader
));
$resources = array(
BASE_DIR.'/config/services.yml',
// ...
);
foreach ($modules as $module) {
$resources[] = $module->getConfigResources();
}
$containerConfig = new SeriveConfigurator($loader);
$containerConfig->configure($container, $resources);
?>
Waarom geef je trouwens 3 loaders mee?