Den glömda historien om OOP

Note: Detta är en del av serien ”Composing Software” (nu en bok!) som handlar om att lära sig funktionell programmering och tekniker för komposition av programvara i JavaScript ES6+ från grunden. Håll dig uppdaterad. Det kommer att komma mycket mer av detta!
Köp boken | Index | < Föregående | Nästa >

De funktionella och imperativa programmeringsparadigmen som vi använder i dag utforskades för första gången matematiskt på 1930-talet med lambda-kalkyl och Turingmaskinen, som är alternativa formuleringar av universell beräkning (formaliserade system som kan utföra allmänna beräkningar). Church Turing-tesen visade att lambda-kalkyl och Turing-maskiner är funktionellt likvärdiga – att allt som kan beräknas med hjälp av en Turing-maskin kan beräknas med hjälp av lambda-kalkyl och vice versa.

Note: Det finns en vanlig missuppfattning att Turing-maskiner kan beräkna allt som går att beräkna. Det finns klasser av problem (t.ex. stoppproblemet) som kan beräknas i vissa fall, men som i allmänhet inte kan beräknas i alla fall med Turingmaskiner. När jag använder ordet ”beräkningsbar” i den här texten menar jag ”beräkningsbar med en Turingmaskin”.

Lambda-kalkyl representerar en uppifrån och ner-princip för funktionstillämpning vid beräkning, medan formuleringen av Turingmaskinen i form av tickerband/registermaskin representerar en nedifrån och upp-princip för imperativ (steg-för-steg) beräkning.

Lågnivåspråk som maskinkod och assembly dök upp på 1940-talet, och i slutet av 1950-talet dök de första populära högnivåspråken upp. Lisp-dialekter är fortfarande vanligt förekommande i dag, inklusive Clojure, Scheme, AutoLISP osv. FORTRAN och COBOL dök båda upp på 1950-talet och är exempel på imperativa högnivåspråk som fortfarande används idag, även om språk i C-familjen har ersatt både COBOL och FORTRAN för de flesta tillämpningar.

Både imperativ programmering och funktionell programmering har sina rötter i matematisk beräkningsteori och föregick digitala datorer. ”Object-Oriented Programming” (OOP) myntades av Alan Kay omkring 1966 eller 1967 när han gick i skolan.

Ivan Sutherlands banbrytande Sketchpad-program var en tidig inspirationskälla för OOP. Den skapades mellan 1961 och 1962 och publicerades i hans Sketchpad Thesis 1963. Objekten var datastrukturer som representerade grafiska bilder som visades på en oscilloskopskärm och hade arv via dynamiska delegater, som Ivan Sutherland kallade ”masters” i sin avhandling. Alla objekt kunde bli en ”master”, och ytterligare instanser av objekten kallades ”occurrences”. Sketchpads masters har mycket gemensamt med JavaScript:s prototypiska arv.

Note: TX-2 vid MIT Lincoln Laboratory var en av de tidiga användningarna av en grafisk datorskärm där man använde sig av direkt skärminteraktion med hjälp av en lätt penna. EDSAC, som var i drift mellan 1948 och 1958, kunde visa grafik på en skärm. Whirlwind vid MIT hade en fungerande oscilloskopskärm 1949. Projektets motiv var att skapa en allmän flygsimulator som kunde simulera instrumentåterkoppling för flera flygplan. Detta ledde till utvecklingen av datasystemet SAGE. TX-2 var en testdator för SAGE.

Det första programmeringsspråket som allmänt erkändes som ”objektorienterat” var Simula, specificerat 1965. Liksom Sketchpad innehöll Simula objekt och introducerade så småningom klasser, klassarv, underklasser och virtuella metoder.

Note: En virtuell metod är en metod som definieras i en klass och som är avsedd att åsidosättas av underklasser. Virtuella metoder gör det möjligt för ett program att anropa metoder som kanske inte finns vid den tidpunkt då koden kompileras genom att använda dynamisk fördelning för att avgöra vilken konkret metod som ska anropas vid körning. JavaScript har dynamiska typer och använder delegeringskedjan för att avgöra vilka metoder som ska anropas, och behöver därför inte exponera begreppet virtuella metoder för programmerare. Med andra ord, alla metoder i JavaScript använder sig av metodfördelning vid körning, så metoder i JavaScript behöver inte deklareras som ”virtuella” för att stödja funktionen.

”Jag hittade på begreppet ’objektorienterad’, och jag kan säga att jag inte tänkte på C++.” ~ Alan Kay, OOPSLA ’97

Alan Kay myntade begreppet ”objektorienterad programmering” vid sin examen 1966 eller 1967. Den stora idén var att använda inkapslade minidatorer i programvara som kommunicerade via meddelandeöverföring snarare än direkt datadelning – för att sluta dela upp program i separata ”datastrukturer” och ”procedurer”.

”Grundprincipen för rekursiv design är att delarna ska ha samma kraft som helheten”. ~ Bob Barton, huvudkonstruktör av B5000, en stordator optimerad för att köra Algol-60.

Smalltalk utvecklades av Alan Kay, Dan Ingalls, Adele Goldberg och andra vid Xerox PARC. Smalltalk var mer objektorienterad än Simula – allt i Smalltalk är ett objekt, inklusive klasser, heltal och block (closures). Den ursprungliga Smalltalk-72 innehöll inte några underklasser. Det infördes i Smalltalk-76 av Dan Ingalls.

Men även om Smalltalk hade stöd för klasser och så småningom även för subklassning, handlade Smalltalk inte om klasser eller subklassning av saker. Det var ett funktionellt språk inspirerat av Lisp och Simula. Alan Kay anser att industrins fokus på subklassning distraherar från de verkliga fördelarna med objektorienterad programmering.

”Jag är ledsen att jag för länge sedan myntade begreppet ”objekt” för det här ämnet eftersom det får många människor att fokusera på den mindre viktiga idén. Den stora idén är meddelandehantering.”
~ Alan Kay

I en e-postväxling från 2003 förtydligade Alan Kay vad han menade när han kallade Smalltalk för ”objektorienterad”:

”OOP betyder för mig bara meddelandehantering, lokal bevarande, skydd och döljande av state-processer, samt extrem sen bindning av alla saker.”
~ Alan Kay

Med andra ord, enligt Alan Kay, är de viktigaste ingredienserna i OOP:

  • Message passing
  • Encapsulation
  • Dynamic binding

Noterbart är att arv och polymorfism i underklasser INTE betraktades som viktiga ingredienser i OOP av Alan Kay, mannen som myntade termen och förde OOP till massorna.

Ett väsentligt inslag i OOP

Kombinationen av meddelandeöverföring och inkapsling tjänar några viktiga syften:

  • Undvika delat föränderligt tillstånd genom att inkapsla tillstånd och isolera andra objekt från lokala tillståndsändringar. Det enda sättet att påverka ett annat objekts tillstånd är att be (inte beordra) det objektet att ändra det genom att skicka ett meddelande. Tillståndsändringar kontrolleras på en lokal, cellulär nivå snarare än att utsättas för delad åtkomst.
  • Frikoppling av objekt från varandra – meddelandesändaren är endast löst kopplad till meddelandemottagaren, genom meddelande-API:t.
  • Anpassningsbarhet och motståndskraft mot förändringar vid körning genom sen bindning. Anpassningsbarhet vid körning ger många stora fördelar som Alan Kay ansåg vara väsentliga för OOP.

Dessa idéer inspirerades av biologiska celler och/eller enskilda datorer i ett nätverk via Alan Kays bakgrund i biologi och inflytande från utformningen av Arpanet (en tidig version av Internet). Redan så tidigt föreställde sig Alan Kay att programvara skulle köras på en gigantisk, distribuerad dator (Internet), där enskilda datorer agerade som biologiska celler, som fungerade oberoende av varandra i sitt eget isolerade tillstånd och kommunicerade via meddelandeöverföring.

”Jag insåg att cell-/helhetsdatormetaforen skulle göra sig av med data”
~ Alan Kay

Med ”göra sig av med data” var Alan Kay säkerligen medveten om problemen med delade, föränderliga tillstånd och den täta koppling som orsakas av delade data – vanliga teman idag.

Men i slutet av 1960-talet var ARPA:s programmerare frustrerade över att behöva välja en datamodellrepresentation för sina program innan de byggde programvaran. Procedurer som var alltför tätt kopplade till särskilda datastrukturer var inte motståndskraftiga mot förändringar. De ville ha en mer homogen behandling av data.

” Hela poängen med OOP är att inte behöva oroa sig för vad som finns inuti ett objekt. Objekt som tillverkas på olika maskiner och med olika språk bör kunna prata med varandra ” ~ Alan Kay

Objekt kan abstrahera och dölja implementationer av datastrukturer. Den interna implementeringen av ett objekt kan ändras utan att andra delar av programvarusystemet bryts. Med extrem sen bindning kan ett helt annat datorsystem ta över ett objekts ansvar och programvaran kan fortsätta att fungera. Objekt kan under tiden visa upp ett standardgränssnitt som fungerar med den datastruktur som objektet råkar använda internt. Samma gränssnitt kan fungera med en länkad lista, ett träd, en ström och så vidare.

Alan Kay såg också objekt som algebraiska strukturer, som ger vissa matematiskt bevisbara garantier för deras beteende:

”Min matematiska bakgrund fick mig att inse att varje objekt skulle kunna ha flera algebraer kopplade till sig, och att det skulle kunna finnas familjer av dessa, och att dessa skulle vara mycket mycket användbara.”
~ Alan Kay

Detta har visat sig vara sant och utgör grunden för objekt som löften och linser, båda inspirerade av kategoriteori.

Den algebraiska karaktären i Alan Kays vision för objekt skulle göra det möjligt för objekt att erbjuda formella verifieringar, deterministiskt beteende och förbättrad testbarhet, eftersom algebror i huvudsak är operationer som lyder några få regler i form av ekvationer.

På programmerarspråk är algebraer som abstraktioner bestående av funktioner (operationer) som åtföljs av särskilda lagar som upprätthålls av enhetstester som dessa funktioner måste klara (axiom/ekvationer).

Dessa idéer glömdes bort i årtionden i de flesta OO-språken i C-familjen, inklusive C++, Java, C#, osv, men de börjar hitta tillbaka i de senaste versionerna av de mest använda OO-språken.

Man kan säga att programmeringsvärlden håller på att återupptäcka fördelarna med funktionell programmering och resonerat tänkande i samband med OO-språken.

Likt JavaScript och Smalltalk före det blir de flesta moderna OO-språken mer och mer ”multiparadigm-språk”. Det finns ingen anledning att välja mellan funktionell programmering och OOP. När vi tittar på den historiska kärnan i var och en av dem är de inte bara kompatibla, utan kompletterande idéer.

Eftersom de har så många egenskaper gemensamt gillar jag att säga att JavaScript är Smalltalks hämnd på världens missförstånd av 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. Typerna spelar ingen roll för .map() eftersom den inte försöker manipulera dem direkt, utan istället tillämpar en funktion som förväntar sig och returnerar de korrekta typerna för applikationen.

// 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);
//

Detta generiska typförhållande är svårt att uttrycka på ett korrekt och grundligt sätt i ett språk som TypeScript, men det var ganska enkelt att uttrycka i Haskells Hindley-Milner-typer med stöd för högre kinded typer (typer av typer).

De flesta typsystem har varit alltför restriktiva för att möjliggöra ett fritt uttryck för dynamiska och funktionella idéer, t.ex. funktionskomposition, fri objektkomposition, förlängning av runtimeobjekt, kombinatorer, linser osv. Med andra ord gör statiska typer ofta det svårare att skriva komponerbar programvara.

Om ditt typsystem är för restriktivt (t.ex. TypeScript, Java) är du tvungen att skriva mer invecklad kod för att uppnå samma mål. Det betyder inte att statiska typer är en dålig idé eller att alla statiska typimplementationer är lika restriktiva. Jag har stött på betydligt färre problem med Haskells typsystem.

Om du är ett fan av statiska typer och inte har något emot restriktionerna, mer kraft till dig, men om du tycker att några av råden i den här texten är svåra eftersom det är svårt att typa sammansatta funktioner och sammansatta algebraiska strukturer, skyller du på typsystemet, inte på idéerna. Folk älskar komforten i sina SUV:er, men ingen klagar på att de inte låter dig flyga. För det behöver du ett fordon med fler frihetsgrader.

Om restriktioner gör din kod enklare, bra! Men om restriktioner tvingar dig att skriva mer komplicerad kod är restriktionerna kanske fel.

Vad är ett objekt?

Objekt har helt klart fått många konnotationer under årens lopp. Det vi kallar ”objekt” i JavaScript är helt enkelt sammansatta datatyper, utan någon av implikationerna från vare sig klassbaserad programmering eller Alan Kays message-passing.

I JavaScript kan dessa objekt stödja inkapsling, meddelandeöverföring, delning av beteende via metoder och till och med polymorfism för underklasser (även om man använder en delegeringskedja snarare än typbaserad avsändning). Du kan tilldela vilken funktion som helst till vilken egenskap som helst. Du kan bygga upp objektbeteenden dynamiskt och ändra betydelsen av ett objekt vid körning. JavaScript stöder också inkapsling med hjälp av closures för att skydda sekretessen för genomförandet. Men allt detta är ett opt-in-beteende.

Vår nuvarande idé om ett objekt är helt enkelt en sammansatt datastruktur och kräver inget mer för att betraktas som ett objekt. Men att programmera med dessa typer av objekt gör inte din kod ”objektorienterad” mer än att programmera med funktioner gör din kod ”funktionell”.

OOP är inte riktig OOP längre

Eftersom ”objekt” i moderna programmeringsspråk betyder mycket mindre än vad det gjorde för Alan Kay, använder jag ”komponent” istället för ”objekt” för att beskriva reglerna för riktig OOP. Många objekt ägs och manipuleras direkt av annan kod i JavaScript, men komponenter bör kapsla in och kontrollera sitt eget tillstånd.

Ekta OOP innebär:

  • Programmering med komponenter (Alan Kays ”objekt”)
  • Komponenternas tillstånd måste kapslas in
  • Användning av meddelandeöverföring för kommunikation mellan objekt
  • Komponenter kan läggas till/ändras/bytas ut vid körning

De flesta komponentbeteenden kan specificeras generiskt med hjälp av algebraiska datastrukturer. Arv är inte nödvändigt här. Komponenter kan återanvända beteenden från delade funktioner och modulära importer utan att dela sina data.

Manipulering av objekt eller användning av klassarv i JavaScript innebär inte att du ”gör OOP”. Att använda komponenter på det här sättet gör det. Men populär användning är hur ord definieras, så kanske borde vi överge OOP och kalla detta för ”Message Oriented Programming (MOP)” i stället för ”Object Oriented Programming (OOP)”?

Är det en slump att moppar används för att städa upp röror?

Hur bra MOP ser ut

I de flesta moderna programvaror finns det ett användargränssnitt som ansvarar för att hantera användarens interaktioner, en del kod som hanterar programtillståndet (användardata) och kod som hanterar system- eller nätverksin/utgång.

Var och en av dessa system kan kräva långlivade processer, t.ex. event listeners, state för att hålla reda på saker som nätverksanslutning, ui elementstatus och själva applikationstillståndet.

Good MOP innebär att i stället för att alla dessa system sträcker sig ut och direkt manipulerar varandras state, kommunicerar systemet med andra komponenter via message dispatch. När användaren klickar på en spara-knapp kan ett "SAVE"-meddelande skickas, som en komponent för programtillstånd kan tolka och vidarebefordra till en handläggare för uppdatering av tillståndet (t.ex. en ren reducerfunktion). När tillståndet har uppdaterats kan tillståndskomponenten skicka ett "STATE_UPDATED"-meddelande till en användargränssnittskomponent, som i sin tur tolkar tillståndet, gör en avstämning av vilka delar av användargränssnittet som behöver uppdateras och vidarebefordrar det uppdaterade tillståndet till de underkomponenter som hanterar de delarna av användargränssnittet.

Under tiden kanske nätverksanslutningskomponenten övervakar användarens anslutning till en annan maskin i nätverket, lyssnar på meddelanden och skickar uppdaterade tillståndsrepresentationer för att spara data på en fjärrmaskin. Den håller internt reda på en timer för nätverkets hjärtslag, om anslutningen för närvarande är online eller offline och så vidare.

Dessa system behöver inte känna till detaljerna i de andra delarna av systemet. Endast om deras individuella, modulära angelägenheter. Systemkomponenterna är dekomponerbara och omkomponerbara. De implementerar standardiserade gränssnitt så att de kan samverka. Så länge gränssnittet är uppfyllt kan man byta ut ersättare som kan göra samma sak på olika sätt, eller helt olika saker med samma meddelanden. Du kan till och med göra det vid körning, och allt skulle fortsätta att fungera korrekt.

Komponenter i samma programvarusystem behöver kanske inte ens finnas på samma maskin. Systemet kan vara decentraliserat. Nätverkslagringen kan dela upp data på ett decentraliserat lagringssystem som IPFS, så att användaren inte är beroende av hälsan hos en viss maskin för att se till att deras data är säkert säkerhetskopierade och skyddade från hackare som kanske vill stjäla dem.

OOP inspirerades delvis av Arpanet, och ett av målen med Arpanet var att bygga ett decentraliserat nätverk som kunde vara motståndskraftigt mot attacker som atombomber. Enligt direktören för DARPA under utvecklingen av Arpanet, Stephen J. Lukasik (”Why the Arpanet Was Built”):

”Målet var att utnyttja ny datorteknik för att tillgodose behoven hos den militära ledningen och kontrollen mot kärnvapenhot, uppnå en överlevnadsduglig kontroll av de amerikanska kärnvapenstyrkorna och förbättra det militära taktiska och ledningsmässiga beslutsfattandet.”

Note: Den främsta drivkraften bakom Arpanet var bekvämlighet snarare än kärnvapenhot, och dess uppenbara försvarsfördelar framkom senare. ARPA använde tre separata datorterminaler för att kommunicera med tre separata datorforskningsprojekt. Bob Taylor ville ha ett enda datornätverk för att ansluta varje projekt till de andra.

Ett bra MOP-system kan dela internets robusthet med hjälp av komponenter som kan bytas ut i heta lägen medan programmet körs. Det skulle kunna fortsätta att fungera om användaren sitter på en mobiltelefon och de går offline för att de gått in i en tunnel. Det skulle kunna fortsätta att fungera om en orkan slår ut strömmen till ett av datacentren där servrarna finns.

Det är dags för programvaruvärlden att släppa det misslyckade klassarvsexperimentet och omfamna de matematiska och vetenskapliga principer som ursprungligen definierade OOP:s anda.

Det är dags för oss att börja bygga mer flexibla, motståndskraftiga och bättre sammansatta programvaror, med MOP och funktionell programmering som arbetar i harmoni.

Notera: MOP-akronymen används redan för att beskriva ”övervakningsorienterad programmering” och det är osannolikt att OOP kommer att försvinna i tysthet.

Var inte upprörd om MOP inte slår igenom som programmeringsspråk.
Do MOP up your OOPs.

Lär dig mer på EricElliottJS.com

Videolektioner om funktionell programmering finns tillgängliga för medlemmar på EricElliottJS.com. Om du inte är medlem kan du registrera dig idag.