Nogmaals, het is volstrekte en zich herhalende idioterie dat leren programmeren verstandig is om digitale basisvaardigheden te leren (het kan eraan bijdragen, maar het is uiterst ineffectief).
Over OOP (Object Oriented Programming): onder de motorkap kijken kan helpen voor de begripsvorming. In de basis is een
object een record met een (onzichtbare) verwijzing naar een tabel met (pointers naar) functies.
(Ik heb al een flink aantal jaren geen C++ of Delphi meer geschreven, corrigeer mij svp als ik domme dingen schrijf).
Hoe zo'n record eruit ziet is gedefinieerd in een "
class", en elke class heeft één zo'n functietabel.
Zo'n tabel met pointers naar functies gekoppeld aan een class wordt een
VFT (Virtual Function Table) genoemd. Dat is niet omdat die functies niet bestaan (dat doen ze allemaal wel), maar omdat elke pointer naar een functie in een VFT naar een functie expliciet gedefinieerd voor de
huidige class kan wijzen, of naar een functie (met dezelfde naam) gedefineerd voor een ouder of voorouder.
Een voorbeeld voor een supermarkt, de class TVoedsel (op de "Borland"-manier zet ik een T van "Type" voor de naam van class-definities en andere "types" om het onderscheid met namen van objecten te verduidelijken):
Class TVoedsel
Velden:
0: PtrTVoedselVFT:TPointer
1: UitersteVerkoopDatum:TDatum
2: VoorraadSoort:TEnum
Functies:
0: Set_UitersteVerkoopDatum(TDatum)
1: Get_UitersteVerkoopDatum() —> TDatum
2: Set_VoorraadSoort(TEnum)
3: Get_VoorraadSoort() —> TEnum
In RAM komt hierdoor (éénmalig per class-definitie):
PtrTVoedselVFT:
0: pointer naar functie 0 van TVoedsel
1: pointer naar functie 1 van TVoedsel
2: pointer naar functie 2 van TVoedsel
3: pointer naar functie 3 van TVoedsel
[...]
Functies:
• code van functie 0 van TVoedsel
• code van functie 1 van TVoedsel
• code van functie 2 van TVoedsel
• code van functie 3 van TVoedsel
Wat "In RAM komt hierdoor" is wat de compiler ervan maakt
vóórdat er objecten van het type TVoedsel zijn aangemaakt.
Ik heb veld 0 ("PtrTVoedselVFT:TPointer") hierboven in schuinschrift geschreven omdat je dit veld, als programmeur, zelf niet ziet (de compiler maakt dat automatisch aan en vult de waarde in voor elk object; voor elk object van hetzelfde type is die waarde identiek, want er bestaat één VFT per classdefinitie).
En met "UitersteVerkoopDatum:TDatum" bedoel ik dat een object (of, zo je wilt, record) van het type TVoedsel (of van een afgeleide daarvan) een veld met de naam "UitersteVerkoopDatum" van het type TDatum zal hebben. Als TDatum als een karakterreeks van 8 tekens is gedefinieerd (voor bijvoorbeeld "20240615"), dan weet de compiler dat er 8 bytes voor dit veld gereserveerd moeten worden (eventueel één extra voor een afsluitende byte met waarde 0, of een andere constructie waarbij bijvoorbeeld de lengte van die karakterreeks
voorafgaand aan die reeks wordt opgeslagen).
VoorraadSoort is bijvoorbeeld
kilogram,
zakken van 5kg,
1 liter drinkpakken etc.
Die basisklasse TVoedsel noemen we een "Virtual Base Class" als je daar
zelf geen objecten van instantieert (je hebt namelijk niks aan een object van het type TVoedsel zonder te weten om wat voor soort voedsel het gaat).
We kunnen nu een class TAardappels definiëren
afgeleid van TVoedsel. Die class "erft" alles van TVoedsel, maar geeft ons de mogelijkheid om daar
nieuwe functies aan toe te voegen. Daarnaast kunnen we ook de pointers naar bestaande functies
wijzigen door functies met bestaande namen te herdefiniëren. Dat doe ik hieronder met twee functies:
Class TAardappels : TVoedsel
Velden: kopie van TVoedsel m.u.v.:
0 PtrTAardappelsVFT:TPointer
Functies: kopie van TVoedsel m.u.v.:
2: Set_VoorraadSoort(TEnum)
3: Get_VoorraadSoort() —> TEnum
In RAM komt hierdoor (éénmalig per class-definitie):
PtrTVoedselVFT:
0: pointer naar functie 0 van TVoedsel
1: pointer naar functie 1 van TVoedsel
2: pointer naar functie 2 van TVoedsel
3: pointer naar functie 3 van TVoedsel
[...]
PtrTAardappelsVFT:
0: pointer naar functie 0 van TVoedsel
1: pointer naar functie 1 van TVoedsel
2: pointer naar functie 2 van TAardappels
3: pointer naar functie 3 van TAardappels
Functies:
• code van functie 0 van TVoedsel
• code van functie 1 van TVoedsel
• code van functie 2 van TVoedsel
• code van functie 3 van TVoedsel
[...]
• code van functie 2 van TAardappels
• code van functie 3 van TAardappels
Gevolg: als je "vanuit" een object van het type TAardappels de functie Get_VoorraadSoort() aanroept, wordt
andere code uitgevoerd dan als je die functie aanroept vanuit een object van het type TWortels (dat je eveneens zou moeten afleiden van TVoedsel).
Twee
objecten van het type TAardappels zullen er, in RAM, uitzien als volgt (daarnaast staan ergens anders in RAM de bovengenoemde tabellen en code éénmalig
per class):
Eerste object:
0: PtrTAardappelsVFT, bijv. 0xBABE1234
1: UitersteVerkoopDatum, bijv. "20240615"
2: VoorraadSoort:TEnum, bijv. ZAK_1KG
Tweede object:
0: PtrTAardappelsVFT, bijv. 0xBABE1234
1: UitersteVerkoopDatum, bijv. "20240701"
2: VoorraadSoort, bijv. PALLET_200KG
Elk object heeft een implicite
this-pointer dat naar veld 0 van dat object wijst.
Waarom OOPHet idee van objectoriëntatie is dat je zoveel mogelijk functionaliteit (inclusief variabelen en constanten) in hogere classes definieert, zodat je minder werk hebt in afgeleide classes, die daardoor overzichtelijker zijn.
En, bijvoorbeeld i.r.t. voorraadbeheer, kun je werken met een lijst van objecten van het type TVoedsel, waarbij elk van die objecten
zelf "weet" van wat voor subtype deze is.
Oftewel, als je de functie Get_VoorraadSoort() aantoept van een object uit een lijst van TVoedsel, en het om een afgeleide TAardappel gaat, dan zorgt de constructie met VFT's ervoor dat,
ondanks de identieke functienaam, de juiste bijpassende functiecode wordt uitgevoerd.
Elk voordeel hep...Maar... zoals altijd heeft alles een prijs: bij het ontwerpen moet je extreem vooruitdenken, en vaak kom je er later achter dat je onverstandige keuzes hebt gemaakt. Ook kan het een drama zijn om te debuggen bij problemen: het kan zeer onduidelijk zijn welke functie (gedefinieerd in welke class) er precies wordt aangeroepen en wanneer.
Dat laatste vooral omdat het vaak verstandig is om vanuit de functie in een afgeleide class
éérst, expliciet dus, de "overruled" functie aan te roepen. Maar als dat niet gebeurt, en een programmeur, die ervan uitgaat dat dit wél zo is of juist niet, iets wijzigt in een functie van een hogere class, heb je de poppen aan het dansen.
Helemaal vervelend kan het worden bij "multiple inheritance", waarbij een class van twee of meer andere is afgeleid.
KortomOOP
lijkt een elegante oplossing om het dupliceren van code te voorkómen, maar mijn ervaring is dat het zoveel nieuwe problemen oplevert dat het alleen in specifieke gevallen de moeite waard is. Uiteindelijk is het toch een CPU die code moet uitvoeren, waarbij een compiler de broncode van (soms extreem abstract denkende) mensen, in iets moet omzetten waar die CPU mee overweg kan.
Kunnen programmeren is geen digitale basisvaardigheid, en OOP is volstrekt zinloze ballast.