Wiskunde
Het klassieke voorbeeld van abstractie in de wiskunde is de constructie van de rationale getallen Q (de breuken) uit de gehele getallen Z. In Z gaan delingen niet altijd op. Daarom heb je extra getallen nodig. Je begint met paren van gehele getallen (t,n). Zo'n paar stelt het getal voor dat je krijgt als je t door n deelt (de paren (x,0) doen niet mee). Maar die verzameling paren is niet abstract, dat wil zeggen dat ze "te veel" eigenschappen hebben. (1,2) is een ander paar dan (2,4), maar stellen wel hetzelfde getal voor (dat valt te bewijzen). Daarom voer je een equivalentierelatie "~" in die aangeeft welke paren hetzelfde getal moeten voorstellen. Deze relatie moet een aantal elementaire eigenschappen heben en bovendien een congruentie voor vermenigvuldigen zijn. Met congruentie bedoel ik dan dat als (t,n) ~ (t',n'), dat dan ook x*(t,n) ~ x*(t',n') .
Als dat allemaal ok is, hebben we een abstractie. Je kijkt niet meer naar de paren als gewone paren, maar door de bril van de equivalentierelatie. Twee paren beschouw je als gelijk als ze equivalent volgens ~ zijn. (Technisch doe je dat door de verzameling van alle paren die equivalent zijn als één getal te beschouwen.) Je hebt dan een nieuwe verzameling gemaakt waarin de eigenschappen van de losse paren verdwenen zijn. De verzameling bevat "minder" elementen en je ziet de teller en de noemer feitelijk niet meer. Je gebruikt ze alleen om de getallen te noteren, maar dat zou je ook anders kunnen doen, bijvoorbeeld met decimale breuken. Dat is nu alleen maar een andere notatie voor dezelfde dingen. De abstractie doet dus wat je wilt, abstraheren van toevallige "implementatie" en het geeft je een nieuw model. In die verzameling Q is zijn vermenigvuldiging en deling (en ook optelling en aftrekking) goed gedefinieerd en het model is gesloten voor die bewerkingen. Je kunt helemaal daarbinnen redeneren. Eigenschappen van de "implementatie" (teller en noemer, of de decimaalontwikkeling), spelen daarbij geen rol meer.
Programmeren
Bij procedures (functies) in programmeertalen vind je een duidelijk voorbeeld van abstractie. (Ik gebruik dit voorbeeld niet omdat het in de Oohaa-discussie aan de orde was.)
Je begint met een stuk code dat een berekening uitvoert met gebruik van een aantal hulpvariabelen, bijvoorbeeld worteltrekken. In de allereenvoudigste vorm zorg je eerst dat in een globale variabele x het getal staat waaruit je de wortel wilt trekken. Vervolgens spring je naar het stuk code die het resultaat in een andere variabele y zet. Dan spring je terug en leest de waarde van y uit.
Deze methode is niet abstract in die zin dat er allerlei specifieke effecten zijn. De variabelen x en y moeten niet ergens anders in gebruik zijn, want die worden overgeschreven. Voor de hulpvariabelen geldt hetzelfde. Een nauwkeurige beschrijving van zo'n "call" moet ook het effect op x, y en de hulp variabelen bevatten.
Daarom voer je parameters, return value en lokale variabelen in. Het gevolg is dat de procedure geen invloed meer heeft op globale variabelen en het is dus werkelijk alleen een functie die de wortel uitrekent. Verschillende implementaties geven hetzelfde gedrag. Een programma dat zulke functies gebruikt is gesloten in de zin dat je het helemaal kunt begrijpen door naar de abstracte beschrijving van de functies te kijken. Zoals bij de constructie van Q beschouw je alle functies die hetzelfde input-outputgedrag hebben als gelijk (of equivalent).
Objecten
Bij objecten kun je beginnen bij records: groepjes globale variabelen die via een gezamenlijk groepsadres (later de object reference geheten) benaderd worden. Dat levert nog geen abstractie op, wel een handige naamgeving. Met encapsulatie kun je proberen abstractie te bereiken. Je verbergt dus de variabelen (=attributen) achter (object-)private access modifiers en je benadert ze via methods. De implementatie van de methods is sowieso niet zichtbaar voor de buitenwereld. Bij de procedures kon je vervolgens het abstracte gedrag in termen van input-output beschrijven. Dat is lekker abstract. Alle procedures die de wortel van de input-parameter opleveren beschouw je als equivalent. Maar hoe doe je dat met methods. Want in het algemeen is een method niet een side-effectloze functie, maar een ding dat de toestand van het object verandert. Dat wil zeggen, de waarden van de variabelen. Daar gaat je abstractie.
Voor niet-losstaande objecten wordt het nog lastiger. Een method heeft dan namelijk niet alleen een verandering in het aangeroepen object tot gevolg, maar mogelijk ook in verbonden objecten, via method calls. De object-encapsulatie is dus niet volledig, itt de encapsulatie van lokale variabelen in procedures.
Wil je toch abstractie krijgen, dan zul je abstracte variabelen moeten invoeren waarin je het gedrag van methods beschrijft. Die zitten wellicht in de hoofden van ontwerpers, maar ze komen er vaak niet uit, heb ik de indruk. Zo heeft een interface (een volledig abstracte klasse)
meestal geen variabelen (in Java mag het zelfs niet), maar abstracte variabelen zou hij wel vaak
moeten hebben. Met verbonden objecten is het me niet helemaal niet duidelijk hoe je het moet doen. Moet je abstracte variabelen koppelen aan een configuratie van objecten? En als die configuraties overlappen, hoe doe je het dan?
Ik benieuwd naar wat de OO-deskundigen hiervan vinden.
2 opmerkingen:
Hmm, dit nodigt uit voor meer reaksies.
Om maar eens te beginnen met die abstracte attributen: ja, in de "abstracte OO-taal" is dat een gangbare zienswijze.
In de OO-programmeertalen niet: dan zijn attributen ook echt programmavariabelen.
Dit is een van de voor de hand liggende verschillen tussen de abstracte OO-taal en de OO-programmeertalen.
Akkoord. Maar het geeft dus wel aan dat er in de OO-programmeertaal niet veel abstractie zit.
Een reactie posten