Stel ik maak een algemene class om data in op te slaan. Vervolgens wil ik ook een class maken om configuratiegegevens in op te slaan. Daar kan ik eigenlijk perfect de algemene Data class voor gebruiken.
Nu vraag ik me iets vreemds af. Wat is nu wijsheid. Om voor de configuratie gebruik te maken van de algemene data class? Dus:
<?php
$config_settings = new Data();
?>
Of is het slimmer om toch een aparte Config class te maken die de Data class extend?
<?php
class Config extends Data {
}
$config = new Config();
?>
Nu hebben we wel een aparte Config class, maar deze is wel leeg. Er staan geen methods is. Wat zou jullie voorkeur hebben?
Dat is ook een mooie oplossing, maar heeft één nadeel. Als je
class PathsCollection implements PathsCollectionInterface
ooit vervangt door bijvoorbeeld
class PathsCollection implements CloudDataMapperInterface
dan heb je elders nog steeds op verschillende plaatsen de (verouderde) typehint PathsCollectionInterface $paths. Het implementeren van de interface zou ik daarom, zoals gezegd, in dit geval liever direct doen via de definitie van de klasse die de interface daadwerkelijk implementeert, niet indirect via op verschillende plaatsen rondzwervende typehints.
Klinkt als een slechte reden. Als je ooit besluit dat PathsCollection stopt met het implementeren van PathsCollectionInterface en in plaats daarvan CloudDataMapperInterface implementeert dan hoor je al besloten te hebben om verder te gaan refactoren omdat je weet dat er dan dingen breken.
Maar vroeg of laat is er altijd een update nodig. Dat is toch wel een zekerheid in software engineering.
Dan heb ik liever één update van één klasse dan tientallen updates van evenveel typehints.
De code hoeft ook niet per se te breken, want je zou kunnen opschalen:
class PathsCollection implements PathsCollectionInterface, CloudDataMapperInterface
Dan ondersteunt de klasse de oude en de nieuwe interface, maar werken de typehints nog steeds met de oude interface. En ja, dat kan zowel een voordeel als een nadeel zijn.
Laten we nog even terugkomen op mijn eerdere vraag. Wat behoort een interface te doen? Hoort een interface alleen aan te geven wat voor methodes de implementing class behoort te hebben? Of moet/mag een interface ook zeggen waar de class voor dient?
Als we weer even terugkomen op een PathsCollection en een ConfigCollection. Beide ondersteunen dezelfde interface DataCollectionInterface. Als ik nu ergens in class Foo een PathsCollection nodig heb, volstaat het dan om te typehinten op DataCollection (hiermee dwingen we de benodigde methodes af) of is het beter om (ook) te typehinten op functionaliteit (ik wil enkel een PathsCollection en dus geen ConfigCollection)?
>> Dat is ook een mooie oplossing, maar heeft één nadeel. ... dan heb je elders nog steeds op verschillende plaatsen de (verouderde) typehint PathsCollectionInterface $paths.
Ward, ik snap precies wat je bedoelt... maar is dit niet altijd het geval?
Misschien begrijp ik je verkeerd, maar als ik het goed begrijp dan hebben we een interface X, en meerdere classes ondersteunen die interface. Dat is handig want dan weten we dat al die classes dezelfde methodes hebben. Als ik van de een op de andere dag besluit dat class Foo een bepaalde interface niet meer ondersteunt, dan lijkt het me logisch dat ik de boel breek. Maar dat is toch in alle gevallen zo?
>> Hoort een interface alleen aan te geven wat voor methodes de implementing class behoort te hebben? Of moet/mag een interface ook zeggen waar de class voor dient?
Dat is eigenlijk hetzelfde. Via constanten beschrijf je wat elke implementatie van de interface "is" en via methoden wat elke implementatie kan "worden" of kan "doen". (Waarmee we voor de liefhebbers en passant mooi even het verschil hebben afgedekt tussen een functie en een procedure.) In dat opzicht lijkt het vrij letterlijk het spelletje Hints: je kunt via hints een ding omschrijven, zolang je het ding zelf maar niet met naam noemt.
>> Misschien begrijp ik je verkeerd, maar als ik het goed begrijp dan hebben we een interface X, en meerdere classes ondersteunen die interface. Dat is handig want dan weten we dat al die classes dezelfde methodes hebben. Als ik van de een op de andere dag besluit dat class Foo een bepaalde interface niet meer ondersteunt, dan lijkt het me logisch dat ik de boel breek. Maar dat is toch in alle gevallen zo?
Dat is ook logisch, maar dan wil ik liever het breekpunt zien in de klasse die de interface implementeert, niet in alle typehints van andere klassen die die ene klasse gebruiken.
Mijn punt is vooral dat als je al een class Foo implements FooInterface hebt, een typehint zoals __construct(FooInterface $foo) of methode(FooInterface $foo) eerder in de weg zit dan iets toevoegt. De implementatie van de interface wordt al direct afgedwongen door de klasse. Het is dubbelop om dat nog eens indirect over te doen met een typehint in alle andere klassen die deze ene klasse gebruiken. En één klasse kun je makkelijker verbouwen dan alle typehints van de interface.
>> Via constanten beschrijf je wat elke implementatie van de interface "is" en via methoden wat elke implementatie kan "worden" of kan "doen".
Wat bedoel je hier met "constanten"? Bedoel je daarmee de naam van de interface?
>> De implementatie van de interface wordt al direct afgedwongen door de klasse.
Ik snap wat je bedoelt. Maar omgekeerd betekent het ook weer dat ik dus overal waar ik naar de class typehint, alleen die ene specifieke class kan gebruiken, en dus geen varianten van de PathsCollection class. Een FooPathsCollection zou dus niet werken, terwijl die bijv. wel de PathsCollectionInterface ondersteunt. Ik meen ooit gehoord te hebben dat je om die reden zoveel mogelijk naar interfaces moet programmeren, maar ik kan me vergissen.
>> Wat bedoel je hier met "constanten"? Bedoel je daarmee de naam van de interface?
Nee, in een interface kun je niet alleen methoden maar ook constanten vastleggen. Daarmee beschrijf je dus niet alleen wat alle objecten met die interface kunnen "worden" (de methoden), maar ook wat ze in eerste aanleg bij hun geboorte al "zijn" (de constanten).
>> Ik snap wat je bedoelt. Maar omgekeerd betekent het ook weer dat ik dus overal waar ik naar de class typehint, alleen die ene specifieke class kan gebruiken, en dus geen varianten van de PathsCollection class.
Ja, maar ik neem aan dat als je een __construct($paths) voor paden nodig hebt, je dat redelijk dicht op de __construct(PathsCollection $paths) class-hint wilt programmeren, niet op de veel ruimere __construct(DataCollection $paths) interface-hint. Doe je dat namelijk niet, dan gooi je alles dat je speciaal voor paden aan PathsCollection hebt toegevoegd overboord.
Bovendien krijg je dan voor paden geen echt PathsCollection-object, maar meer een generiek DataCollection-object en zou je dus, per ongeluk, ook een ConfigCollection-object kunnen gebruiken waar je eigenlijk een PathsCollection-object nodig hebt.
>> Daarmee beschrijf je dus niet alleen wat alle objecten met die interface kunnen "worden" (de methoden), maar ook wat ze in eerste aanleg bij hun geboorte al "zijn" (de constanten).
Kun je hiervan een voorbeeldje geven?
>> Ja, maar... nodig hebt.
Helemaal mee eens. Maar waarom zou je niet een PathsCollectionInterface maken?
Ah oke helder... in wat voor situaties zou je dat in de praktijk nodig kunnen hebben?
Wat betreft mijn andere vraag...
Helemaal mee eens. Maar waarom zou je niet een PathsCollectionInterface maken?
Kun je dit nog toelichten? Ik weet ook niet of het een goed idee is om een PathsCollectionInterface te maken. Want ben je dan straks niet bezig om voor iedere class een interface te maken? Dat lijkt me ook weer niet de bedoeling. Maar waar leg je dan die grens? Anders gezegd: hoe bepaal je of je moet typehinten naar een class of naar een interface? Kunnen we daar niet een mooie regel voor bedenken? Dat zou wel zeer behulpzaam zijn denk ik.