Az OOP elfeledett története
Megjegyzés: Ez a könyv a “Composing Software” sorozat része (most már könyv!), amely a funkcionális programozás és a kompozíciós szoftvertechnikák JavaScript ES6+-ban való elsajátításáról szól az alapoktól. Maradjon velünk. Még sok minden fog következni belőle!
Vásárolja meg a könyvet | Index | < Previous | Next >
A ma használt funkcionális és imperatív programozási paradigmákat először az 1930-as években fedezték fel matematikailag a lambda-kalkulussal és a Turing-géppel, amelyek az univerzális számítás alternatív megfogalmazásai (formalizált rendszerek, amelyek képesek általános számításokat végezni). A Church Turing-tézis megmutatta, hogy a lambda-kalkulus és a Turing-gépek funkcionálisan egyenértékűek – bármi, ami kiszámítható Turing-géppel, kiszámítható lambda-kalkulussal is, és fordítva.
Megjegyzés: Gyakori tévhit, hogy a Turing-gépek bármit kiszámíthatnak, ami kiszámítható. Vannak olyan problémaosztályok (pl. a megállási probléma), amelyek bizonyos esetekben kiszámíthatóak, de általában nem számíthatóak ki minden esetben Turing-gépekkel. Amikor ebben a szövegben a “kiszámítható” szót használom, akkor a “Turing-gép által kiszámítható”-t értem.
A lambdakalkulus a számítás felülről lefelé irányuló, függvényalkalmazásos megközelítését képviseli, míg a Turing-gép futószalagos/regiszteres gépi megfogalmazása a számítás alulról felfelé irányuló, imperatív (lépésről lépésre történő) megközelítését.
Az 1940-es években jelentek meg az olyan alacsony szintű nyelvek, mint a gépi kód és az assembly, az 1950-es évek végére pedig megjelentek az első népszerű magas szintű nyelvek. A Lisp nyelvek dialektusai ma is elterjedtek, beleértve a Clojure-t, a Scheme-t, az AutoLISP-t stb. A FORTRAN és a COBOL is az 1950-es években jelent meg, és mindkettő a ma is használatos imperatív magas szintű nyelvek példája, bár a C-családba tartozó nyelvek a legtöbb alkalmazásban felváltották mind a COBOL-t, mind a FORTRAN-t.
Az imperatív programozás és a funkcionális programozás egyaránt a számításelmélet matematikájában gyökerezik, még a digitális számítógépek előtt. Az “objektumorientált programozás” (OOP) fogalmát Alan Kay alkotta meg 1966 vagy 1967 körül, amikor még az egyetemen végzett.
Ivan Sutherland korszakalkotó Sketchpad alkalmazása volt az OOP korai inspirációja. A program 1961 és 1962 között készült, és 1963-ban jelent meg a Sketchpad Thesis című szakdolgozatában. Az objektumok olyan adatstruktúrák voltak, amelyek egy oszcilloszkóp képernyőjén megjelenített grafikus képeket ábrázoltak, és dinamikus delegáltakon keresztül történő örökléssel rendelkeztek, amelyeket Ivan Sutherland a dolgozatában “mestereknek” nevezett. Bármely objektum “mesterré” válhatott, és az objektumok további példányait “előfordulásoknak” nevezték. A Sketchpad mesterei sok közös vonást mutatnak a JavaScript prototípusos öröklésével.
Megjegyzés: A TX-2 az MIT Lincoln Laboratóriumában a grafikus számítógépes monitor egyik korai alkalmazása volt, amely közvetlen képernyőinterakciót alkalmazott egy könnyű toll segítségével. Az EDSAC, amely 1948-1958 között működött, képes volt grafikát megjeleníteni a képernyőn. Az MIT-ben működő Whirlwind 1949-ben működőképes oszcilloszkópos kijelzővel rendelkezett. A projekt motivációja az volt, hogy egy olyan általános repülésszimulátort hozzanak létre, amely képes szimulálni több repülőgép műszeres visszajelzését. Ez vezetett a SAGE számítógépes rendszer kifejlesztéséhez. A TX-2 a SAGE tesztszámítógépe volt.
Az első széles körben “objektumorientáltnak” elismert programozási nyelv az 1965-ben specifikált Simula volt. A Sketchpadhez hasonlóan a Simula is objektumokat tartalmazott, és végül bevezette az osztályokat, az osztályöröklést, az alosztályokat és a virtuális metódusokat.
Megjegyzés: A virtuális metódus egy osztályon definiált metódus, amelyet arra terveztek, hogy az alosztályok felülbírálják. A virtuális metódusok lehetővé teszik a program számára, hogy olyan metódusokat hívjon meg, amelyek a kód lefordításának pillanatában esetleg nem léteznek, azáltal, hogy dinamikus diszpozíciót alkalmaznak annak meghatározására, hogy a futásidőben melyik konkrét metódust kell meghívni. A JavaScript dinamikus típusokkal rendelkezik, és a delegálási láncot használja a meghívandó metódusok meghatározására, így nincs szükség arra, hogy a virtuális metódusok fogalmát a programozók elé tárja. Másképpen fogalmazva, a JavaScriptben minden metódus futásidejű metódusdiszpatchet használ, így a JavaScriptben a metódusokat nem kell “virtuálisnak” deklarálni a funkció támogatásához.
“Én találtam ki az “objektumorientált” kifejezést, és elmondhatom, hogy nem a C++-ra gondoltam.” ~ Alan Kay, OOPSLA ’97
Alan Kay találta ki az “objektumorientált programozás” kifejezést 1966-ban vagy 1967-ben az egyetemen. A nagy ötlet az volt, hogy olyan kapszulázott miniszámítógépeket használjunk a szoftverekben, amelyek közvetlen adatmegosztás helyett üzenetátadással kommunikálnak – hogy a programokat ne bontsuk tovább különálló “adatstruktúrákra” és “eljárásokra”.
“A rekurzív tervezés alapelve, hogy a részeknek ugyanolyan erejük legyen, mint az egésznek”. ~ Bob Barton, az Algol-60 futtatására optimalizált nagyszámítógép, a B5000 fő tervezője.
A Smalltalkot Alan Kay, Dan Ingalls, Adele Goldberg és mások fejlesztették a Xerox PARC-ben. A Smalltalk objektumorientáltabb volt, mint a Simula – a Smalltalkban minden objektum, beleértve az osztályokat, egészeket és blokkokat (closures). Az eredeti Smalltalk-72 nem rendelkezett alosztályozással. Ezt a Smalltalk-76-ban vezette be Dan Ingalls.
Míg a Smalltalk támogatta az osztályokat és végül az alosztályozást, a Smalltalk nem az osztályokról vagy az alosztályozásról szólt. Ez egy funkcionális nyelv volt, amelyet a Lisp és a Simula is inspirált. Alan Kay úgy véli, hogy az iparágnak az alosztályozásra való összpontosítása elvonja a figyelmet az objektumorientált programozás valódi előnyeitől.
“Sajnálom, hogy régen én találtam ki az “objektumok” kifejezést erre a témára, mert ez sokakat arra késztet, hogy a kevésbé fontos gondolatra koncentráljanak. A nagy ötlet az üzenetküldés.”
~ Alan Kay
Egy 2003-as e-mailváltásban Alan Kay tisztázta, mire gondolt, amikor a Smalltalkot “objektumorientáltnak” nevezte:
“Az OOP számomra csak üzenetküldést, helyi megtartást és az állapot-folyamatok védelmét és elrejtését, valamint minden dolog extrém késői megkötését jelenti.”
~ Alan Kay
Más szóval, Alan Kay szerint az OOP alapvető összetevői a következők:
- Message passing
- Encapsulation
- Dynamic binding
Megjegyzendő, hogy az öröklést és az alosztály-polimorfizmust NEM tartotta az OOP alapvető összetevőinek Alan Kay, az az ember, aki kitalálta a kifejezést és megismertette az OOP-t a tömegekkel.
Az OOP lényege
Az üzenetátadás és a kapszulázás kombinációja néhány fontos célt szolgál:
- A megosztott, változékony állapot elkerülése az állapot kapszulázásával és más objektumok elkülönítése a helyi állapotváltozásoktól. Egy másik objektum állapotát csak úgy lehet befolyásolni, ha üzenet küldésével megkérjük (nem parancsoljuk) az adott objektumot, hogy változtassa meg azt. Az állapotváltozásokat helyi, sejtszinten ellenőrzik, nem pedig kiteszik a megosztott hozzáférésnek.
- Az objektumok egymástól való függetlenítése – az üzenetküldő csak lazán van összekapcsolva az üzenetvevővel, az üzenetküldő API-n keresztül.
- Alkalmazkodóképesség és rugalmasság a futásidejű változásokhoz a késői kötés révén. A futásidejű alkalmazkodóképesség számos nagyszerű előnyt biztosít, amelyeket Alan Kay az OOP szempontjából alapvető fontosságúnak tartott.
Az ötleteket Alan Kay biológiai háttere és az Arpanet (az internet korai változata) tervezésének hatása révén a biológiai sejtek és/vagy a hálózaton lévő egyes számítógépek ihlették. Alan Kay már ekkor is egy óriási, elosztott számítógépen (az interneten) futó szoftvert képzelt el, ahol az egyes számítógépek biológiai sejtekként viselkedtek, önállóan működtek a saját elszigetelt állapotukban, és üzenettovábbítással kommunikáltak.
“Rájöttem, hogy a sejt/egész számítógép metafora megszabadul az adatoktól”
~ Alan Kay
Az “adatoktól való megszabadulás” alatt Alan Kay bizonyára tisztában volt a megosztott, változtatható állapot problémáival és a megosztott adatok okozta szoros csatolással – ezek ma is gyakori témák.
Az 1960-as évek végén azonban az ARPA programozóit frusztrálta, hogy a szoftver építése előtt adatmodell-reprezentációt kellett választaniuk a programjaikhoz. Az egyes adatstruktúrákhoz túl szorosan kapcsolódó eljárások nem voltak rugalmasak a változásokkal szemben. Az adatok homogénebb kezelését akarták.
” Az OOP lényege, hogy ne kelljen azzal foglalkozni, mi van egy objektum belsejében. A különböző gépeken és különböző nyelveken készült objektumoknak képesnek kell lenniük arra, hogy beszéljenek egymással ” ~ Alan Kay
Az objektumok absztrahálhatják és elrejthetik az adatszerkezetek megvalósítását. Egy objektum belső implementációja változhat anélkül, hogy a szoftverrendszer más részei megszakadnának. Sőt, extrém késői kötéssel egy teljesen más számítógépes rendszer átveheti egy objektum feladatait, és a szoftver továbbra is működhet. Az objektumok eközben egy olyan szabványos interfészt tehetnek közzé, amely bármilyen adatstruktúrával működik, amelyet az objektum történetesen belsőleg használ. Ugyanez az interfész működhet egy összekapcsolt listával, egy fával, egy folyamattal és így tovább.
Alan Kay az objektumokat algebrai struktúráknak is tekintette, amelyek bizonyos matematikailag bizonyítható garanciákat adnak a viselkedésükre:
“A matematikai hátterem miatt rájöttem, hogy minden objektumhoz több algebra is tartozhat, és ezekből lehetnek családok, és hogy ezek nagyon-nagyon hasznosak lennének.”
~ Alan Kay
Ez igaznak bizonyult, és olyan objektumok alapját képezi, mint az ígéretek és a lencsék, amelyeket a kategóriaelmélet ihletett.
Alan Kay objektumokra vonatkozó elképzelésének algebrai természete lehetővé tenné, hogy az objektumok formális igazolásokat, determinisztikus viselkedést és jobb tesztelhetőséget engedjenek meg maguknak, mivel az algebrák lényegében olyan műveletek, amelyek egyenletek formájában engedelmeskednek néhány szabálynak.
A programozói szakzsargonban az algebrák olyanok, mint a függvényekből (műveletekből) álló absztrakciók, amelyeket olyan speciális törvények kísérnek, amelyeket a függvényeknek egységtesztekkel kell teljesíteniük (axiómák/egyenletek).
Ezeket az elképzeléseket évtizedekig elfelejtették a legtöbb C-családba tartozó OO nyelvben, beleértve a C++, Java, C# stb. nyelveket, de kezdenek visszatalálni a legelterjedtebb OO nyelvek legújabb változataiba.
Mondhatnánk, hogy a programozói világ újra felfedezi a funkcionális programozás és az ésszerű gondolkodás előnyeit az OO nyelvek kontextusában.
A legtöbb modern OO nyelv, mint korábban a JavaScript és a Smalltalk, egyre inkább “többparadigmás nyelvvé” válik. Nincs okunk választani a funkcionális programozás és az OOP között. Ha megnézzük mindkettő történelmi lényegét, akkor nemcsak kompatibilis, hanem egymást kiegészítő eszmék.
Miatt, hogy annyi közös vonásuk van, szeretem azt mondani, hogy a JavaScript a Smalltalk bosszúja a világ OOP-ról alkotott félreértésén. 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. A típusok nem számítanak a .map()
számára, mert nem próbálja közvetlenül manipulálni őket, hanem egy olyan függvényt alkalmaz, amely az alkalmazásnak megfelelő típusokat vár és ad vissza.
// 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);
//
Ezt az általános típuskapcsolatot nehéz helyesen és alaposan kifejezni egy olyan nyelvben, mint a TypeScript, de elég könnyű volt kifejezni a Haskell Hindley Milner típusaiban a magasabb rendű típusok (típusok típusai) támogatásával.
A legtöbb típusrendszer túlságosan korlátozó volt ahhoz, hogy lehetővé tegye a dinamikus és funkcionális ötletek szabad kifejezését, mint például a függvénykompozíció, a szabad objektumkompozíció, a futásidejű objektumbővítés, a kombinátorok, a lencsék stb. Más szóval, a statikus típusok gyakran megnehezítik a komponálható szoftverek írását.
Ha a típusrendszer túlságosan korlátozó (pl. TypeScript, Java), akkor kénytelenek vagyunk bonyolultabb kódot írni ugyanazon célok eléréséhez. Ez nem jelenti azt, hogy a statikus típusok rossz ötlet, vagy hogy minden statikus típusimplementáció egyformán korlátozó. Én sokkal kevesebb problémával találkoztam a Haskell típusrendszerével.
Ha a statikus típusok rajongója vagy, és nem zavarnak a korlátozások, akkor több erőt neked, de ha a szövegben található tanácsok egy részét nehéznek találod, mert nehéz összetett függvényeket és összetett algebrai struktúrákat tipizálni, akkor a típusrendszert hibáztasd, ne az ötleteket. Az emberek szeretik a terepjáróik kényelmét, de senki sem panaszkodik, hogy nem engednek repülni. Ehhez több szabadságfokkal rendelkező járműre van szükség.
Ha a korlátozások egyszerűbbé teszik a kódot, nagyszerű! De ha a korlátozások arra kényszerítenek, hogy bonyolultabb kódot írj, akkor talán a korlátozások helytelenek.
Mi az objektum?
Az objektumok az évek során egyértelműen sokféle jelentéstartalmat kaptak. Amit a JavaScriptben “objektumoknak” nevezünk, az egyszerűen összetett adattípusok, amelyeknek nincs semmi köze sem az osztályalapú programozáshoz, sem Alan Kay üzenetátviteléhez.
A JavaScriptben ezek az objektumok támogathatják és gyakran támogatják is a kapszulázást, az üzenetátadást, a viselkedés megosztását metódusokon keresztül, sőt még az alosztályok polimorfizmusát is (bár nem típusalapú diszpécser, hanem delegálási láncot használnak). Bármilyen függvényt hozzárendelhetsz bármilyen tulajdonsághoz. Az objektumok viselkedését dinamikusan építhetjük fel, és futás közben megváltoztathatjuk egy objektum jelentését. A JavaScript támogatja a kapszulázást is, a megvalósítás titkosságát biztosító closures használatával. De mindez opcionális viselkedés.
Az objektumról alkotott jelenlegi elképzelésünk egyszerűen egy összetett adatszerkezet, és nem igényel semmi többet ahhoz, hogy objektumnak tekintsük. De az ilyen típusú objektumok használatával történő programozás nem teszi a kódot “objektumorientálttá”, mint ahogy a függvényekkel történő programozás sem teszi a kódot “funkcionálissá”.
Az OOP már nem igazi OOP
Mert mivel az “objektum” a modern programozási nyelvekben sokkal kevesebbet jelent, mint Alan Kay számára, az “objektum” helyett a “komponens” szót használom a valódi OOP szabályainak leírására. A JavaScriptben sok objektumot más kód közvetlenül birtokol és manipulál, de a komponenseknek kapszulázniuk és irányítaniuk kell a saját állapotukat.
A valódi OOP azt jelenti:
- Programozás komponensekkel (Alan Kay “objektuma”)
- A komponensek állapotát kapszulázni kell
- Üzenetátadás használata az objektumok közötti kommunikációhoz
- A komponensek futásidőben hozzáadhatók/megváltoztathatók/cserélhetők
A legtöbb komponens viselkedése algebrai adatstruktúrák segítségével általánosan megadható. Öröklődésre itt nincs szükség. A komponensek újra felhasználhatják a megosztott függvényekből és moduláris importokból származó viselkedéseket anélkül, hogy megosztanák az adataikat.
Az objektumok manipulálása vagy az osztályöröklés használata JavaScriptben nem jelenti azt, hogy “OOP-t csinálsz”. A komponensek ilyen módon történő használata igen. De a népszerű használat az, ahogyan a szavakat definiálják, így talán el kellene hagynunk az OOP-t, és “objektumorientált programozás (OOP)” helyett “üzenetorientált programozásnak (MOP)” nevezni ezt?
Véletlen, hogy a felmosórongyokat rendetlenségek feltakarítására használják?
Milyen a jó MOP
A legtöbb modern szoftverben van egy felhasználói felület, amely a felhasználói interakciók kezeléséért felelős, van egy kód, amely az alkalmazás állapotát (felhasználói adatok), és van kód, amely a rendszer- vagy hálózati I/O-t kezeli.
Ezek a rendszerek mindegyike igényelhet hosszú életű folyamatokat, például eseményhallgatókat, állapotot, hogy nyomon kövesse az olyan dolgokat, mint a hálózati kapcsolat, az ui elem állapota és maga az alkalmazás állapota.
A jó MOP azt jelenti, hogy ahelyett, hogy ezek a rendszerek egymás állapotát közvetlenül manipulálnák, a rendszer üzenetek küldésével kommunikál a többi komponenssel. Amikor a felhasználó rákattint egy mentés gombra, egy "SAVE"
üzenetet küldhet, amelyet egy alkalmazásállapot-komponens értelmezhet és továbbíthat egy állapotfrissítési kezelőnek (például egy tiszta redukáló függvénynek). Esetleg az állapot frissítése után az állapotkomponens elküldhet egy "STATE_UPDATED"
üzenetet egy UI-komponensnek, amely viszont értelmezi az állapotot, összehangolja, hogy a UI mely részeit kell frissíteni, és továbbítja a frissített állapotot a UI ezen részeit kezelő alkomponenseknek.
Eközben a hálózati kapcsolati komponens figyelheti a felhasználó kapcsolatát egy másik géphez a hálózaton, figyelheti az üzeneteket, és továbbíthatja a frissített állapotreprezentációkat az adatok távoli gépen történő mentése érdekében. Belsőleg nyomon követi a hálózati szívverés időzítőjét, hogy a kapcsolat éppen online vagy offline van-e, és így tovább.
Ezeknek a rendszereknek nem kell tudniuk a rendszer többi részének részleteiről. Csak az egyéni, modulárisan felépített gondjaikról. A rendszerelemek dekomponálhatók és újrakomponálhatók. Szabványosított interfészeket valósítanak meg, hogy képesek legyenek együttműködni. Amíg az interfész megfelel, addig helyettesíthetjük a helyettesítő komponenseket, amelyek ugyanazt a dolgot más módon, vagy teljesen más dolgokat végezhetnek ugyanazokkal az üzenetekkel. Ezt akár futás közben is megteheti, és minden továbbra is megfelelően működne.
Az azonos szoftverrendszer komponenseinek még csak nem is feltétlenül kell ugyanazon a gépen elhelyezkedniük. A rendszer lehet decentralizált. A hálózati tároló feloszthatná az adatokat egy decentralizált tárolórendszerben, például az IPFS-ben, így a felhasználó nem függne egy adott gép állapotától, hogy az adatai biztonságosan legyenek mentve, és biztonságban legyenek a hackerektől, akik esetleg el akarják lopni őket.
A OOP-t részben az Arpanet ihlette, és az Arpanet egyik célja az volt, hogy olyan decentralizált hálózatot építsen, amely ellenáll az atombombához hasonló támadásoknak. Az Arpanet fejlesztése során a DARPA igazgatója, Stephen J. Lukasik szerint (“Why the Arpanet Was Built”):
“A cél az volt, hogy az új számítógépes technológiákat kihasználva kielégítsék a katonai vezetés és irányítás nukleáris fenyegetésekkel szembeni igényeit, elérjék az amerikai nukleáris erők túlélhető irányítását, és javítsák a katonai taktikai és vezetői döntéshozatalt.”
Megjegyzés: Az Arpanet elsődleges mozgatórugója nem a nukleáris fenyegetés, hanem a kényelem volt, és nyilvánvaló védelmi előnyei később jelentek meg. Az ARPA három különálló számítógépes terminált használt három különálló számítógépes kutatási projekttel való kommunikációra. Bob Taylor egyetlen számítógépes hálózatot akart, amely összekapcsolja az egyes projekteket a többivel.
A jó MOP-rendszer osztozhat az internet robusztusságában olyan komponensek használatával, amelyek az alkalmazás futása közben is cserélhetők. Tovább működhetne, ha a felhasználó mobiltelefonon van, és offline állapotba kerül, mert belépett egy alagútba. Tovább működhet, ha egy hurrikán kiüti az áramot az egyik adatközpontban, ahol a szerverek találhatók.
Itt az ideje, hogy a szoftvervilág elengedje a sikertelen osztályöröklési kísérletet, és elfogadja a matematikai és tudományos alapelveket, amelyek eredetileg az OOP szellemét határozták meg.
Itt az ideje, hogy rugalmasabb, ellenállóbb, jobban összeállított szoftvereket kezdjünk építeni, a MOP és a funkcionális programozás harmóniájában.
Megjegyzés: A MOP rövidítést már használják a “monitoring-orientált programozás” leírására, és nem valószínű, hogy az OOP csendben eltűnik.
Ne bosszankodjon, ha a MOP nem terjed el a programozási szaknyelvben.
Tegye MOP-pal az OOP-ját.
Tanuljon többet az EricElliottJS.com-on
A funkcionális programozásról szóló videoleckék elérhetőek az EricElliottJS.com tagjai számára. Ha még nem vagy tag, iratkozz fel még ma.