De vergeten geschiedenis van OOP

Note: dit maakt deel uit van de serie “Composing Software” (nu een boek!) over het vanaf de basis aanleren van functioneel programmeren en compositionele softwaretechnieken in JavaScript ES6+. Blijf op de hoogte. Er komt nog veel meer van dit!
Koop het boek | Index | < Vorige | Volgende >

De functionele en imperatieve programmeerparadigma’s die we tegenwoordig gebruiken, werden voor het eerst wiskundig verkend in de jaren 1930 met lambda calculus en de Turing machine, dat zijn alternatieve formuleringen van universele computatie (geformaliseerde systemen die algemene computatie kunnen uitvoeren). De Church Turing Thesis toonde aan dat lambda calculus en Turing machines functioneel equivalent zijn – dat alles wat met een Turing machine berekend kan worden, ook met lambda calculus berekend kan worden, en vice versa.

Note: Er is een veel voorkomende misvatting dat Turing machines alles kunnen berekenen wat berekenbaar is. Er zijn klassen van problemen (b.v. het halteringsprobleem) die voor sommige gevallen berekenbaar kunnen zijn, maar die over het algemeen niet voor alle gevallen berekenbaar zijn met behulp van Turing machines. Wanneer ik in deze tekst het woord “berekenbaar” gebruik, bedoel ik “berekenbaar door een Turing machine”.

Lambda calculus vertegenwoordigt een top-down, functietoepassing benadering van berekening, terwijl de ticker tape/register machine formulering van de Turing machine een bottom-up, imperatieve (stap-voor-stap) benadering van berekening vertegenwoordigt.

Laag-niveau talen zoals machinecode en assemblage verschenen in de jaren 1940, en tegen het einde van de jaren 1950 verschenen de eerste populaire hoog-niveau talen. Lisp-dialecten worden vandaag de dag nog steeds veel gebruikt, waaronder Clojure, Scheme, AutoLISP, enz. FORTRAN en COBOL verschenen beide in de jaren 1950 en zijn voorbeelden van imperatieve hoog-niveau talen die vandaag de dag nog steeds in gebruik zijn, hoewel de C-familie talen zowel COBOL als FORTRAN voor de meeste toepassingen hebben vervangen.

Zowel imperatief programmeren als functioneel programmeren hebben hun wortels in de wiskunde van de rekentheorie, die dateert van voor de digitale computers. “Object-Oriented Programming” (OOP) is bedacht door Alan Kay rond 1966 of 1967, toen hij nog op de universiteit zat.

Ivan Sutherland’s baanbrekende Sketchpad applicatie was een vroege inspiratie voor OOP. Het werd gemaakt tussen 1961 en 1962 en gepubliceerd in zijn Sketchpad Thesis in 1963. De objecten waren datastructuren die grafische beelden voorstelden die op een oscilloscoopscherm werden weergegeven, en ze waren voorzien van overerving via dynamische gedelegeerden, die Ivan Sutherland in zijn proefschrift “masters” noemde. Elk object kon een “master” worden, en bijkomende instanties van de objecten werden “occurrences” genoemd. De masters van Sketchpad hebben veel gemeen met de prototypische overerving van JavaScript.

Note: De TX-2 in het MIT Lincoln Laboratory was een van de eerste toepassingen van een grafische computermonitor die gebruik maakte van directe scherminteractie met behulp van een lichte pen. De EDSAC, die tussen 1948-1958 operationeel was, kon grafische afbeeldingen op een scherm weergeven. De Whirlwind van het MIT beschikte in 1949 over een werkend oscilloscoopscherm. De motivatie van het project was het creëren van een algemene vluchtsimulator die in staat was instrumentfeedback voor meerdere vliegtuigen te simuleren. Dat leidde tot de ontwikkeling van het SAGE-computersysteem. De TX-2 was een testcomputer voor SAGE.

De eerste programmeertaal die algemeen werd erkend als “objectgeoriënteerd” was Simula, gespecificeerd in 1965. Net als Sketchpad bevatte Simula objecten en uiteindelijk klassen, overerving van klassen, subklassen en virtuele methoden.

Note: Een virtuele methode is een methode die is gedefinieerd in een klasse en die is ontworpen om te worden overschreven door subklassen. Met virtuele methoden kan een programma methoden aanroepen die misschien niet bestaan op het moment dat de code wordt gecompileerd, door gebruik te maken van dynamische dispatch om te bepalen welke concrete methode tijdens runtime moet worden aangeroepen. JavaScript heeft dynamische types en gebruikt de delegatieketen om te bepalen welke methoden moeten worden aangeroepen, zodat het concept van virtuele methoden niet aan programmeurs hoeft te worden blootgesteld. Anders gezegd, alle methoden in JavaScript gebruiken runtime method dispatch, dus methoden in JavaScript hoeven niet “virtueel” te worden verklaard om de functie te ondersteunen.

“Ik heb de term ‘objectgeoriënteerd’ verzonnen, en ik kan je vertellen dat ik C++ niet in gedachten had.” ~ Alan Kay, OOPSLA ’97

Alan Kay bedacht de term “objectgeoriënteerd programmeren” tijdens zijn studie in 1966 of 1967. Het grote idee was om ingekapselde mini-computers te gebruiken in software die communiceerde via message passing in plaats van directe gegevensuitwisseling – om programma’s niet langer op te splitsen in afzonderlijke “gegevensstructuren” en “procedures”.

“Het basisprincipe van recursief ontwerpen is om de delen dezelfde kracht te geven als het geheel.” ~ Bob Barton, de hoofdontwerper van de B5000, een mainframe geoptimaliseerd om Algol-60 te draaien.

Smalltalk werd ontwikkeld door Alan Kay, Dan Ingalls, Adele Goldberg, en anderen bij Xerox PARC. Smalltalk was meer object-georiënteerd dan Simula – alles in Smalltalk is een object, inclusief klassen, gehele getallen, en blokken (closures). Het oorspronkelijke Smalltalk-72 had geen subclassing. Dat werd geïntroduceerd in Smalltalk-76 door Dan Ingalls.

Hoewel Smalltalk classes en uiteindelijk subclassing ondersteunde, ging Smalltalk niet over classes of het subclassen van dingen. Het was een functionele taal, geïnspireerd door zowel Lisp als Simula. Alan Kay beschouwt de focus van de industrie op subclassing als een afleiding van de werkelijke voordelen van objectgeoriënteerd programmeren.

“Het spijt me dat ik lang geleden de term “objecten” heb bedacht voor dit onderwerp, omdat het veel mensen ertoe brengt zich te concentreren op het minder belangrijke idee. Het grote idee is messaging.”
~ Alan Kay

In een e-mailwisseling uit 2003 verduidelijkte Alan Kay wat hij bedoelde toen hij Smalltalk “objectgeoriënteerd” noemde:

“OOP betekent voor mij alleen messaging, lokale retentie en bescherming en verbergen van state-processen, en extreme late-binding van alle dingen.”
~ Alan Kay

Met andere woorden, volgens Alan Kay zijn de essentiële ingrediënten van OOP:

  • Message passing
  • Encapsulation
  • Dynamic binding

Notably, inheritance and subclass polymorphism were NOT considered essential ingredients of OOP by Alan Kay, the man who coined the term and brought OOP to the masses.

De essentie van OOP

De combinatie van message passing en encapsulation dient enkele belangrijke doelen:

  • Het vermijden van gedeelde mutable state door state in te kapselen en andere objecten te isoleren van lokale state veranderingen. De enige manier om de toestand van een ander object te beïnvloeden is om dat object te vragen (niet commanderen) om het te veranderen door een bericht te sturen. Toestandsveranderingen worden gecontroleerd op een lokaal, cellulair niveau in plaats van blootgesteld aan gedeelde toegang.
  • Objecten van elkaar loskoppelen – de berichtverzender is slechts losjes gekoppeld aan de berichtontvanger, via de messaging API.
  • Aanpasbaarheid en veerkracht voor veranderingen tijdens runtime via late binding. Runtime aanpasbaarheid biedt vele grote voordelen die Alan Kay als essentieel beschouwde voor OOP.

Deze ideeën werden geïnspireerd door biologische cellen en/of individuele computers op een netwerk via Alan Kay’s achtergrond in de biologie en invloed van het ontwerp van Arpanet (een vroege versie van het internet). Al in een vroeg stadium stelde Alan Kay zich voor dat software zou draaien op een gigantische, gedistribueerde computer (het internet), waar individuele computers zich gedroegen als biologische cellen, onafhankelijk opererend op hun eigen geïsoleerde staat, en communicerend via message passing.

“Ik realiseerde me dat de cel/gehele-computer-metafoor zou afrekenen met gegevens”
~ Alan Kay

Met “afrekenen met gegevens” was Alan Kay zich zeker bewust van problemen met gedeelde muteerbare toestanden en de strakke koppeling die wordt veroorzaakt door gedeelde gegevens – thema’s die vandaag de dag veel voorkomen.

Maar aan het eind van de jaren zestig werden de programmeurs van ARPA gefrustreerd door de noodzaak om een datamodel te kiezen voor hun programma’s voordat ze software gingen bouwen. Procedures die te nauw gekoppeld waren aan bepaalde gegevensstructuren waren niet bestand tegen verandering. Zij wilden een meer homogene behandeling van gegevens.

” het hele punt van OOP is dat je je geen zorgen hoeft te maken over wat er in een object zit. Objecten die op verschillende machines en met verschillende talen zijn gemaakt, moeten met elkaar kunnen praten” ~ Alan Kay

Objecten kunnen de implementatie van gegevensstructuren abstraheren en verbergen. De interne implementatie van een object kan veranderen zonder andere delen van het softwaresysteem te breken. In feite zou, met extreme late binding, een geheel ander computersysteem de verantwoordelijkheden van een object kunnen overnemen, en de software zou kunnen blijven werken. Objecten zouden ondertussen een standaard interface kunnen tonen die werkt met iedere datastructuur die het object intern gebruikt. Dezelfde interface zou kunnen werken met een gekoppelde lijst, een boom, een stroom, enzovoort.

Alan Kay zag objecten ook als algebraïsche structuren, die bepaalde wiskundig bewijsbare garanties geven over hun gedrag:

“Door mijn wiskundige achtergrond realiseerde ik me dat elk object verschillende algebra’s met zich mee zou kunnen brengen, en dat er families van deze zouden kunnen zijn, en dat deze heel erg nuttig zouden zijn.”
~ Alan Kay

Dit is waar gebleken, en vormt de basis voor objecten als beloften en lenzen, beide geïnspireerd door de categorietheorie.

De algebraïsche aard van Alan Kay’s visie op objecten zou objecten in staat stellen zich formele verificaties, deterministisch gedrag en verbeterde testbaarheid te veroorloven, omdat algebra’s in essentie operaties zijn die gehoorzamen aan een paar regels in de vorm van vergelijkingen.

In programmeursjargon zijn algebra’s een soort abstracties die bestaan uit functies (operaties) vergezeld van specifieke wetten die worden afgedwongen door unit tests waaraan die functies moeten voldoen (axioma’s/vergelijkingen).

Deze ideeën zijn decennia lang vergeten in de meeste OO talen van de C-familie, waaronder C++, Java, C#, enz, maar ze beginnen hun weg terug te vinden in recente versies van de meest gebruikte OO talen.

Je zou kunnen zeggen dat de programmeerwereld de voordelen van functioneel programmeren en beredeneerd denken herontdekt in de context van OO talen.

Net als JavaScript en Smalltalk daarvoor, worden de meeste moderne OO talen meer en meer “multi-paradigma talen”. Er is geen reden om te kiezen tussen functioneel programmeren en OOP. Als we kijken naar de historische essentie van elk, zijn ze niet alleen compatibel, maar complementaire ideeën.

Omdat ze zoveel kenmerken gemeen hebben, zeg ik graag dat JavaScript de wraak van Smalltalk is op het misverstand van de wereld over OOP. Both Smalltalk and JavaScript support:

  • Objects
  • First-class functions and closures
  • Dynamic types
  • Late binding (functions/methods changeable at runtime)
  • OOP without class inheritance

What is essential to OOP (according to Alan Kay)?

  • Encapsulation
  • Message passing
  • Dynamic binding (the ability for the program to evolve/adapt at runtime)

What is non-essential?

  • Classes
  • Class inheritance
  • Special treatment for objects/functions/data
  • The new keyword
  • Polymorphism
  • Static types
  • Recognizing a class as a “type”

If your background is Java or C#, you may be thinking static types and Polymorphism are essential ingredients, but Alan Kay preferred dealing with generic behaviors in algebraic form. For example, from Haskell:

fmap :: (a -> b) -> f a -> f b

This is the functor map signature, which acts generically over unspecified types a and b, applying a function from a to b in the context of a functor of a to produce a functor of b. Functor is math jargon that essentially means “supporting the map operation”. If you’re familiar with .map() in JavaScript, you already know what that means.

Here are two examples in JavaScript:

// isEven = Number => Boolean
const isEven = n => n % 2 === 0;const nums = ;// map takes a function `a => b` and an array of `a`s (via `this`)
// and returns an array of `b`s.
// in this case, `a` is `Number` and `b` is `Boolean`
const results = nums.map(isEven);console.log(results);
//

The .map() method is generic in the sense that a and b can be any type, and .map() handles it just fine because arrays are data structures that implement the algebraic functor laws. De types doen er niet toe voor .map() omdat het niet probeert ze direct te manipuleren, maar in plaats daarvan een functie toepast die de juiste types voor de toepassing verwacht en retourneert.

// matches = a => Boolean
// here, `a` can be any comparable type
const matches = control => input => input === control;const strings = ;const results = strings.map(matches('bar'));console.log(results);
//

Deze generieke type-relatie is moeilijk om correct en grondig uit te drukken in een taal als TypeScript, maar was vrij eenvoudig uit te drukken in Haskell’s Hindley Milner types met ondersteuning voor higher kinded types (types van types).

De meeste typesystemen zijn te beperkend geweest om vrije expressie van dynamische en functionele ideeën mogelijk te maken, zoals functiecompositie, vrije objectcompositie, runtime objectuitbreiding, combinators, lenzen, enzovoort. Met andere woorden, statische types maken het vaak moeilijker om samenstelbare software te schrijven.

Als je typesysteem te restrictief is (bijv. TypeScript, Java), ben je gedwongen om meer ingewikkelde code te schrijven om dezelfde doelen te bereiken. Dat betekent niet dat statische types een slecht idee zijn, of dat alle implementaties van statische types even beperkend zijn. Ik ben veel minder problemen tegengekomen met het typesysteem van Haskell.

Als je een fan bent van statische types en je vindt de beperkingen niet erg, veel succes, maar als je sommige adviezen in deze tekst moeilijk vindt omdat het moeilijk is om samengestelde functies en samengestelde algebraïsche structuren te typen, geef dan het typesysteem de schuld, niet de ideeën. Mensen houden van het comfort van hun SUV’s, maar niemand klaagt dat je er niet mee kunt vliegen. Daarvoor heb je een voertuig nodig met meer vrijheidsgraden.

Als beperkingen je code eenvoudiger maken, prima! Maar als beperkingen je dwingen om ingewikkelder code te schrijven, zijn de beperkingen misschien verkeerd.

Wat is een Object?

Objecten hebben in de loop der jaren duidelijk een heleboel connotaties gekregen. Wat wij “objecten” noemen in JavaScript zijn gewoon samengestelde datatypen, zonder de implicaties van op klassen gebaseerde programmering of Alan Kay’s message-passing.

In JavaScript kunnen die objecten inkapseling, het doorgeven van berichten, het delen van gedrag via methoden en zelfs polymorfisme van subklassen ondersteunen (zij het met behulp van een delegatieketen in plaats van op type gebaseerde dispatch). Je kunt elke functie toewijzen aan elke eigenschap. Je kunt objectgedrag dynamisch opbouwen, en de betekenis van een object tijdens runtime veranderen. JavaScript ondersteunt ook inkapseling met behulp van closures voor privacy bij de implementatie. Maar dat is allemaal opt-in gedrag.

Ons huidige idee van een object is eenvoudigweg een samengestelde datastructuur, en vereist niets meer om als object te worden beschouwd. Maar programmeren met dit soort objecten maakt je code net zo min “object-georiënteerd” als programmeren met functies je code “functioneel” maakt.

OOP is niet meer echt OOP

Omdat “object” in moderne programmeertalen veel minder betekent dan het deed voor Alan Kay, gebruik ik “component” in plaats van “object” om de regels van echte OOP te beschrijven. Veel objecten zijn eigendom van en worden direct gemanipuleerd door andere code in JavaScript, maar componenten moeten hun eigen toestand inkapselen en controleren.

Echte OOP betekent:

  • Programmeren met componenten (Alan Kay’s “object”)
  • De toestand van componenten moet worden ingekapseld
  • Gebruik message passing voor inter-object communicatie
  • Componenten kunnen worden toegevoegd/veranderd/vervangen tijdens runtime

De meeste gedragingen van componenten kunnen generiek worden gespecificeerd met behulp van algebraïsche datastructuren. Inheritance is hier niet nodig. Componenten kunnen gedrag van gedeelde functies en modulaire imports hergebruiken zonder hun gegevens te delen.

Het manipuleren van objecten of het gebruik van klasse overerving in JavaScript betekent niet dat je “aan OOP doet”. Het gebruik van componenten op deze manier wel. Maar het populaire gebruik bepaalt hoe woorden worden gedefinieerd, dus misschien moeten we OOP maar loslaten en dit “Message Oriented Programming (MOP)” noemen in plaats van “Object Oriented Programming (OOP)”?

Is het toeval dat dweilen wordt gebruikt om rommel op te ruimen?

Waar goed MOP op lijkt

In de meeste moderne software is er een UI die verantwoordelijk is voor het beheer van de interacties tussen gebruikers, een code die de toestand van de toepassing (gebruikersgegevens) beheert, en een code die het systeem of de netwerk-I/O beheert.

Elk van deze systemen kan processen met een lange levensduur nodig hebben, zoals event listeners, state voor het bijhouden van zaken als de netwerkverbinding, ui element status, en de applicatie state zelf.

Goede MOP betekent dat in plaats van al deze systemen uit te reiken en direct elkaars state te manipuleren, het systeem communiceert met andere componenten via message dispatch. Als de gebruiker op een save-knop klikt, wordt een "SAVE" bericht verzonden, dat een applicatie state component kan interpreteren en doorsturen naar een state update handler (zoals een pure reducer functie). Misschien kan de state component na het bijwerken van de state een "STATE_UPDATED" bericht naar een UI component sturen, die op zijn beurt de state interpreteert, uitzoekt welke delen van de UI moeten worden bijgewerkt, en de bijgewerkte state doorgeeft aan de subcomponenten die deze delen van de UI afhandelen.

Tussen kan de netwerk connection component de verbinding van de gebruiker met een andere machine op het netwerk in de gaten houden, luisteren naar berichten, en bijgewerkte state representaties versturen om gegevens op een andere machine op te slaan. Het houdt intern een netwerk heartbeat timer bij, of de verbinding momenteel online of offline is, enzovoort.

Deze systemen hoeven niets te weten over de details van de andere delen van het systeem. Alleen over hun individuele, modulaire zorgen. De systeemcomponenten kunnen worden ontleed en opnieuw worden samengesteld. Ze implementeren gestandaardiseerde interfaces, zodat ze kunnen samenwerken. Zolang aan de interface wordt voldaan, kun je vervangers in de plaats stellen die hetzelfde kunnen doen op verschillende manieren, of totaal verschillende dingen met dezelfde berichten.

Componenten van hetzelfde softwaresysteem hoeven zich misschien niet eens op dezelfde machine te bevinden. Het systeem zou gedecentraliseerd kunnen zijn. De netwerkopslag zou de gegevens kunnen verdelen over een gedecentraliseerd opslagsysteem als IPFS, zodat de gebruiker niet afhankelijk is van de gezondheid van een bepaalde machine om er zeker van te zijn dat er een veilige back-up van zijn gegevens is, en veilig voor hackers die ze zouden willen stelen.

OOP is gedeeltelijk geïnspireerd door Arpanet, en een van de doelen van Arpanet was om een gedecentraliseerd netwerk te bouwen dat bestand zou zijn tegen aanvallen als atoombommen. Volgens Stephen J. Lukasik, directeur van DARPA tijdens de ontwikkeling van Arpanet (“Why the Arpanet Was Built”):

“Het doel was om nieuwe computertechnologieën te exploiteren om te voldoen aan de behoeften van militaire commandovoering en controle tegen nucleaire dreigingen, om een overleefbare controle van Amerikaanse nucleaire strijdkrachten te bereiken en om de militaire tactische en managementbeslissingen te verbeteren.”

Note: De primaire impuls van Arpanet was eerder gemak dan nucleaire dreiging, en de duidelijke defensievoordelen kwamen pas later naar voren. ARPA gebruikte drie afzonderlijke computerterminals om te communiceren met drie afzonderlijke computeronderzoeksprojecten. Bob Taylor wilde één enkel computernetwerk om elk project met de anderen te verbinden.

Een goed MOP systeem zou de robuustheid van het internet kunnen delen door gebruik te maken van componenten die hot-swappable zijn terwijl de applicatie draait. Het zou kunnen blijven werken als de gebruiker een mobiele telefoon gebruikt en hij offline gaat omdat hij een tunnel is ingegaan. Het kan blijven werken als een orkaan de stroom uitschakelt naar een van de datacentra waar servers staan.

Het is tijd voor de software wereld om het mislukte class inheritance experiment los te laten, en de wiskundige en wetenschappelijke principes te omarmen die oorspronkelijk de geest van OOP definieerden.

Het is tijd voor ons om te beginnen met het bouwen van meer flexibele, meer veerkrachtige, beter samengestelde software, met MOP en functioneel programmeren in harmonie samenwerkend.

Note: Het acroniem MOP wordt al gebruikt om “monitoring-oriented programming” te beschrijven en het is onwaarschijnlijk dat OOP stilletjes zal verdwijnen.

Wees niet boos als MOP niet aanslaat als programmeertaal.
MOP je OOP’s wel.

Learn More at EricElliottJS.com

Video lessen over functioneel programmeren zijn beschikbaar voor leden van EricElliottJS.com. Als u geen lid bent, meld u dan vandaag nog aan.