Istoria uitată a OOP

Nota: Aceasta face parte din seria „Composing Software” (acum o carte!) despre învățarea de la zero a programării funcționale și a tehnicilor de software compozițional în JavaScript ES6+. Rămâneți pe fază. Vor urma multe altele!
Cumpărați cartea | Index | < Precedent | Următor >

Paradigmele de programare funcțională și imperativă pe care le folosim astăzi au fost explorate pentru prima dată din punct de vedere matematic în anii 1930 cu ajutorul calculului lambda și al mașinii Turing, care sunt formulări alternative ale calculului universal (sisteme formalizate care pot efectua calcule generale). Teza Church Turing a arătat că calculul lambda și mașinile Turing sunt echivalente din punct de vedere funcțional – că orice lucru care poate fi calculat cu ajutorul unei mașini Turing poate fi calculat cu ajutorul calculului lambda și viceversa.

Nota: Există o concepție greșită comună conform căreia mașinile Turing pot calcula orice lucru calculabil. Există clase de probleme (de exemplu, problema halting-ului) care pot fi calculabile pentru anumite cazuri, dar care, în general, nu sunt calculabile pentru toate cazurile cu ajutorul mașinilor Turing. Când folosesc cuvântul „calculabil” în acest text, mă refer la „calculabil de către o mașină Turing”.

Calculul Lambda reprezintă o abordare de sus în jos, de aplicare a funcțiilor la calcul, în timp ce formularea de tip bandă de bifurcație/mașină de înregistrare a mașinii Turing reprezintă o abordare de jos în sus, imperativă (pas cu pas) a calculului.

Limbajele de nivel inferior, cum ar fi codul mașină și asamblarea, au apărut în anii 1940, iar la sfârșitul anilor 1950 au apărut primele limbaje populare de nivel înalt. Dialectele Lisp sunt încă utilizate în mod curent în prezent, inclusiv Clojure, Scheme, AutoLISP etc. FORTRAN și COBOL au apărut amândouă în anii 1950 și sunt exemple de limbaje imperative de nivel înalt încă utilizate în prezent, deși limbajele din familia C au înlocuit atât COBOL, cât și FORTRAN pentru majoritatea aplicațiilor.

Atât programarea imperativă, cât și programarea funcțională își au rădăcinile în matematica teoriei calculului, precedând calculatoarele digitale. „Programarea orientată pe obiecte” (OOP – Object-Oriented Programming) a fost inventată de Alan Kay în jurul anului 1966 sau 1967, în timp ce era la facultate.

Aplicația de referință Sketchpad a lui Ivan Sutherland a fost o sursă de inspirație timpurie pentru OOP. Aceasta a fost creată între 1961 și 1962 și publicată în teza sa Sketchpad în 1963. Obiectele erau structuri de date reprezentând imagini grafice afișate pe ecranul unui osciloscop și prezentau moștenirea prin delegați dinamici, pe care Ivan Sutherland i-a numit „maeștri” în teza sa. Orice obiect putea deveni un „maestru”, iar instanțele suplimentare ale obiectelor erau numite „apariții”. Maeștrii din Sketchpad au multe în comun cu moștenirea prototipală din JavaScript.

Nota: TX-2 de la MIT Lincoln Laboratory a fost una dintre primele utilizări ale unui monitor grafic de calculator care folosea interacțiunea directă cu ecranul cu ajutorul unui stilou ușor. EDSAC, care a fost operațional între 1948-1958 putea afișa grafice pe un ecran. Whirlwind de la MIT avea un afișaj osciloscopic funcțional în 1949. Motivația proiectului a fost aceea de a crea un simulator de zbor generalist capabil să simuleze feedback-ul instrumentelor pentru mai multe aeronave. Acest lucru a dus la dezvoltarea sistemului de calcul SAGE. TX-2 a fost un computer de testare pentru SAGE.

Primul limbaj de programare recunoscut pe scară largă ca fiind „orientat pe obiecte” a fost Simula, specificat în 1965. Ca și Sketchpad, Simula a prezentat obiecte și, în cele din urmă, a introdus clase, moștenirea claselor, subclase și metode virtuale.

Nota: O metodă virtuală este o metodă definită pe o clasă care este concepută pentru a fi suprascrisă de subclase. Metodele virtuale permit unui program să apeleze metode care ar putea să nu existe în momentul compilării codului prin utilizarea dispecerizării dinamice pentru a determina ce metodă concretă trebuie invocată la momentul execuției. JavaScript dispune de tipuri dinamice și utilizează lanțul de delegare pentru a determina ce metode trebuie invocate, astfel încât nu este nevoie să expună programatorilor conceptul de metode virtuale. Altfel spus, toate metodele din JavaScript utilizează dispecerizarea metodelor în timpul execuției, astfel încât metodele din JavaScript nu au nevoie să fie declarate „virtuale” pentru a susține această caracteristică.

„Eu am inventat termenul „orientat pe obiecte” și pot să vă spun că nu am avut în minte C++.” ~ Alan Kay, OOPSLA ’97

Alan Kay a inventat termenul de „programare orientată pe obiecte” la facultate, în 1966 sau 1967. Ideea cea mare era de a folosi mini-computere încapsulate în software care comunicau prin trecerea mesajelor mai degrabă decât prin partajarea directă a datelor – pentru a nu mai descompune programele în „structuri de date” și „proceduri” separate.

„Principiul de bază al proiectării recursive este de a face ca părțile să aibă aceeași putere ca și întregul.” ~ Bob Barton, principalul proiectant al B5000, un mainframe optimizat pentru a rula Algol-60.

Smalltalk a fost dezvoltat de Alan Kay, Dan Ingalls, Adele Goldberg și alții la Xerox PARC. Smalltalk a fost mai orientat pe obiecte decât Simula – totul în Smalltalk este un obiect, inclusiv clase, numere întregi și blocuri (closures). Versiunea originală Smalltalk-72 nu includea subclasa. Aceasta a fost introdusă în Smalltalk-76 de către Dan Ingalls.

În timp ce Smalltalk susținea clasele și, în cele din urmă, subclasarea, Smalltalk nu era despre clase sau despre subclasarea lucrurilor. A fost un limbaj funcțional inspirat de Lisp, precum și de Simula. Alan Kay consideră că accentul pus de industrie pe subclasare este o distragere a atenției de la adevăratele beneficii ale programării orientate pe obiecte.

„Îmi pare rău că am inventat cu mult timp în urmă termenul „obiecte” pentru acest subiect, deoarece îi face pe mulți oameni să se concentreze pe o idee mai puțin importantă. Ideea mare este mesageria.”
~ Alan Kay

Într-un schimb de e-mail din 2003, Alan Kay a clarificat ce a vrut să spună atunci când a numit Smalltalk „orientat pe obiecte”:

„OOP pentru mine înseamnă doar mesagerie, păstrarea locală și protecția și ascunderea proceselor de stare și legarea extrem de târzie a tuturor lucrurilor.”
~ Alan Kay

Cu alte cuvinte, potrivit lui Alan Kay, ingredientele esențiale ale OOP sunt:

  • Message passing
  • Encapsularea
  • Legatura dinamică

În mod notabil, moștenirea și polimorfismul subclasei NU au fost considerate ingrediente esențiale ale OOP de către Alan Kay, omul care a inventat termenul și a adus OOP în rândul maselor.

Esența OOP

Combinația de trecere a mesajelor și încapsulare servește unor scopuri importante:

  • Evitarea stării mutabile partajate prin încapsularea stării și izolarea altor obiecte de modificările locale de stare. Singurul mod de a afecta starea unui alt obiect este de a cere (nu de a comanda) acelui obiect să o schimbe prin trimiterea unui mesaj. Modificările de stare sunt controlate la un nivel local, celular, mai degrabă decât expuse accesului partajat.
  • Decuplarea obiectelor unele de altele – expeditorul de mesaje este cuplat doar slab la receptorul de mesaje, prin intermediul API-ului de mesagerie.
  • Adaptabilitate și reziliență la modificări în timpul execuției prin legarea târzie. Adaptabilitatea în timp de execuție oferă multe beneficii deosebite pe care Alan Kay le considera esențiale pentru OOP.

Aceste idei au fost inspirate de celulele biologice și/sau de calculatoarele individuale dintr-o rețea prin prisma pregătirii lui Alan Kay în domeniul biologiei și a influenței din proiectarea Arpanet (o versiune timpurie a internetului). Chiar și așa de timpuriu, Alan Kay și-a imaginat software-ul rulând pe un computer gigantic, distribuit (internetul), în care calculatoarele individuale acționau ca niște celule biologice, funcționând independent în propria lor stare izolată și comunicând prin trecerea mesajelor.

„Mi-am dat seama că metafora celulei/computerelor întregi ar scăpa de date”
~ Alan Kay

Prin „a scăpa de date”, Alan Kay era cu siguranță conștient de problemele legate de starea mutabilă partajată și de cuplarea strânsă cauzată de datele partajate – teme comune astăzi.

Dar, la sfârșitul anilor 1960, programatorii ARPA erau frustrați de necesitatea de a alege o reprezentare a modelului de date pentru programele lor înainte de a construi software-ul. Procedurile care erau prea strâns cuplate la anumite structuri de date nu erau rezistente la schimbări. Ei doreau un tratament mai omogen al datelor.

” scopul OOP este de a nu trebui să ne facem griji cu privire la ceea ce se află în interiorul unui obiect. Obiectele realizate pe mașini diferite și cu limbaje diferite ar trebui să fie capabile să vorbească între ele ” ~ Alan Kay

Obiectele pot face abstracție și pot ascunde implementările structurilor de date. Implementarea internă a unui obiect ar putea fi schimbată fără a sparge alte părți ale sistemului software. De fapt, cu o legare extrem de târzie, un sistem informatic complet diferit ar putea prelua responsabilitățile unui obiect, iar software-ul ar putea continua să funcționeze. Între timp, obiectele ar putea expune o interfață standard care să funcționeze cu orice structură de date pe care obiectul a folosit-o în interior. Aceeași interfață ar putea funcționa cu o listă legată, un arbore, un flux și așa mai departe.

Alan Kay a văzut, de asemenea, obiectele ca structuri algebrice, care oferă anumite garanții demonstrabile din punct de vedere matematic cu privire la comportamentele lor:

„Pregătirea mea în matematică m-a făcut să îmi dau seama că fiecare obiect ar putea avea mai multe algebre asociate cu el, că ar putea exista familii de astfel de structuri și că acestea ar fi foarte, foarte utile.”
~ Alan Kay

Acest lucru s-a dovedit a fi adevărat și stă la baza unor obiecte precum promisiunile și lentilele, ambele inspirate de teoria categoriilor.

Natura algebrică a viziunii lui Alan Kay pentru obiecte ar permite obiectelor să își permită verificări formale, comportament determinist și testabilitate îmbunătățită, deoarece algebrele sunt în esență operații care se supun câtorva reguli sub formă de ecuații.

În jargonul programatorilor, algebrele sunt ca niște abstracțiuni alcătuite din funcții (operații) însoțite de legi specifice impuse de testele unitare pe care acele funcții trebuie să le treacă (axiome/ecuații).

Aceste idei au fost uitate timp de decenii în majoritatea limbajelor OO din familia C, inclusiv C++, Java, C#, etc., dar ele încep să se regăsească în versiunile recente ale celor mai utilizate limbaje OO.

Ați putea spune că lumea programării redescoperă beneficiile programării funcționale și ale gândirii raționale în contextul limbajelor OO.

Ca și JavaScript și Smalltalk înainte, majoritatea limbajelor OO moderne devin din ce în ce mai mult „limbaje multiparadigmatice”. Nu există niciun motiv pentru a alege între programarea funcțională și OOP. Când ne uităm la esența istorică a fiecăruia, acestea nu sunt doar idei compatibile, ci și complementare.

Pentru că au atât de multe caracteristici comune, îmi place să spun că JavaScript este răzbunarea lui Smalltalk pe neînțelegerea lumii cu privire la 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. Tipurile nu contează pentru .map() pentru că nu încearcă să le manipuleze direct, aplicând în schimb o funcție care așteaptă și returnează tipurile corecte pentru aplicație.

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

Această relație generică de tip este dificil de exprimat corect și complet într-un limbaj precum TypeScript, dar a fost destul de ușor de exprimat în tipurile Hindley Milner din Haskell cu suport pentru tipuri cu tipare superioare (tipuri de tipuri).

Majoritatea sistemelor de tipuri au fost prea restrictive pentru a permite exprimarea liberă a ideilor dinamice și funcționale, cum ar fi compoziția funcțiilor, compoziția liberă a obiectelor, extinderea obiectelor în timp de execuție, combinatorii, lentilele etc. Cu alte cuvinte, tipurile statice îngreunează în mod frecvent scrierea de software compozabil.

Dacă sistemul de tipuri este prea restrictiv (de exemplu, TypeScript, Java), sunteți forțat să scrieți un cod mai alambicat pentru a atinge aceleași obiective. Asta nu înseamnă că tipurile statice sunt o idee proastă sau că toate implementările de tipuri statice sunt la fel de restrictive. Am întâlnit mult mai puține probleme cu sistemul de tipuri al lui Haskell.

Dacă sunteți un fan al tipurilor statice și nu vă deranjează restricțiile, mai multă putere pentru dumneavoastră, dar dacă unele dintre sfaturile din acest text vi se par dificile pentru că este greu să tastați funcții compuse și structuri algebrice compuse, dați vina pe sistemul de tipuri, nu pe idei. Oamenii iubesc confortul SUV-urilor lor, dar nimeni nu se plânge că nu te lasă să zbori. Pentru asta, ai nevoie de un vehicul cu mai multe grade de libertate.

Dacă restricțiile îți fac codul mai simplu, minunat! Dar dacă restricțiile vă forțează să scrieți un cod mai complicat, poate că restricțiile sunt greșite.

Ce este un obiect?

Evident, obiectele au căpătat o mulțime de conotații de-a lungul anilor. Ceea ce noi numim „obiecte” în JavaScript sunt pur și simplu tipuri de date compozite, fără niciuna dintre implicațiile din programarea bazată pe clase sau trecerea de mesaje a lui Alan Kay.

În JavaScript, aceste obiecte pot suporta și suportă în mod frecvent încapsularea, transmiterea de mesaje, partajarea comportamentului prin metode, chiar și polimorfismul subclasei (deși utilizează un lanț de delegare mai degrabă decât dispecerizarea bazată pe tip). Puteți atribui orice funcție oricărei proprietăți. Puteți construi comportamente ale obiectelor în mod dinamic și puteți schimba semnificația unui obiect în timpul execuției. JavaScript suportă, de asemenea, încapsularea folosind închideri pentru confidențialitatea implementării. Dar toate acestea sunt comportamente de tip „opt-in”.

Ideea noastră actuală de obiect este pur și simplu o structură de date compozită și nu necesită nimic în plus pentru a fi considerat un obiect. Dar programarea folosind aceste tipuri de obiecte nu vă face codul „orientat pe obiecte”, așa cum programarea cu funcții nu vă face codul „funcțional”.

OOP nu mai este OOP reală

Pentru că „obiect” în limbajele de programare moderne înseamnă mult mai puțin decât însemna pentru Alan Kay, voi folosi „componentă” în loc de „obiect” pentru a descrie regulile OOP reală. Multe obiecte sunt deținute și manipulate direct de alt cod în JavaScript, dar componentele ar trebui să își încapsuleze și să își controleze propria stare.

OOP reală înseamnă:

  • Programarea cu componente („obiectul” lui Alan Kay)
  • Starea componentelor trebuie să fie încapsulată
  • Utilizarea transmiterii de mesaje pentru comunicarea între obiecte
  • Componentele pot fi adăugate/schimbate/înlocuite în timpul execuției

Majoritatea comportamentelor componentelor pot fi specificate în mod generic folosind structuri de date algebrice. Moștenirea nu este necesară în acest caz. Componentele pot reutiliza comportamente din funcții partajate și importuri modulare fără a partaja datele lor.

Manipularea obiectelor sau utilizarea moștenirii de clasă în JavaScript nu înseamnă că „faceți OOP”. Utilizarea componentelor în acest mod o face. Dar utilizarea populară este modul în care se definesc cuvintele, așa că poate ar trebui să renunțăm la OOP și să numim acest lucru „Programare orientată pe mesaje (MOP)” în loc de „Programare orientată pe obiecte (OOP)”?

Este oare o coincidență faptul că mopurile sunt folosite pentru a curăța mizeria?

Cum arată o bună MOP

În majoritatea software-ului modern, există o interfață de utilizator responsabilă pentru gestionarea interacțiunilor cu utilizatorul, un cod care gestionează starea aplicației (datele utilizatorului) și un cod care gestionează I/O de sistem sau de rețea.

Care dintre aceste sisteme poate necesita procese cu durată lungă de viață, cum ar fi ascultătorii de evenimente, starea pentru a ține evidența unor lucruri cum ar fi conexiunea de rețea, starea elementului ui și starea aplicației în sine.

Un bun MOP înseamnă că, în loc ca toate aceste sisteme să ajungă și să manipuleze direct starea fiecăruia dintre ele, sistemul comunică cu alte componente prin intermediul expedierii de mesaje. Atunci când utilizatorul face clic pe un buton de salvare, ar putea fi expediat un mesaj "SAVE", pe care o componentă de stare a aplicației ar putea să-l interpreteze și să-l retransmită către un gestionar de actualizare a stării (cum ar fi o funcție de reductor pur). Poate că, după ce starea a fost actualizată, componenta de stare ar putea expedia un mesaj "STATE_UPDATED" către o componentă UI, care, la rândul său, va interpreta starea, va reconcilia ce părți ale UI trebuie actualizate și va transmite starea actualizată către subcomponentele care gestionează acele părți ale UI.

Între timp, componenta de conectare la rețea ar putea monitoriza conexiunea utilizatorului la o altă mașină din rețea, să asculte mesaje și să expediaze reprezentări actualizate ale stării pentru a salva date pe o mașină la distanță. În mod intern, ține evidența unui cronometru de bătaie a inimii rețelei, dacă conexiunea este în prezent online sau offline și așa mai departe.

Aceste sisteme nu au nevoie să cunoască detaliile celorlalte părți ale sistemului. Doar despre preocupările lor individuale, modulare. Componentele sistemului sunt decompozabile și recompozabile. Ele implementează interfețe standardizate, astfel încât să poată interopera. Atâta timp cât interfața este satisfăcută, se pot înlocui cu înlocuitori care pot face același lucru în moduri diferite, sau lucruri complet diferite cu aceleași mesaje. Puteți face acest lucru chiar și în timpul execuției, iar totul ar continua să funcționeze în mod corespunzător.

Componentele aceluiași sistem software pot să nu fie nici măcar nevoite să fie localizate pe aceeași mașină. Sistemul ar putea fi descentralizat. Rețeaua de stocare ar putea împărți datele pe un sistem de stocare descentralizat, cum ar fi IPFS, astfel încât utilizatorul să nu depindă de sănătatea unei anumite mașini pentru a se asigura că datele sale sunt salvate în siguranță și în siguranță față de hackerii care ar putea dori să le fure.

OOP a fost parțial inspirat de Arpanet, iar unul dintre obiectivele Arpanet a fost acela de a construi o rețea descentralizată care ar putea fi rezistentă la atacuri precum bombele atomice. Potrivit directorului DARPA în timpul dezvoltării Arpanet, Stephen J. Lukasik („Why the Arpanet Was Built”):

„Scopul a fost acela de a exploata noile tehnologii informatice pentru a răspunde nevoilor de comandă și control militar împotriva amenințărilor nucleare, de a realiza un control supraviețuitor al forțelor nucleare americane și de a îmbunătăți procesul de luare a deciziilor militare tactice și de management.”

Nota: Principalul impuls al Arpanet a fost mai degrabă comoditatea decât amenințarea nucleară, iar avantajele sale evidente în materie de apărare au apărut mai târziu. ARPA folosea trei terminale de calculatoare separate pentru a comunica cu trei proiecte de cercetare informatică separate. Bob Taylor dorea o singură rețea de calculatoare care să conecteze fiecare proiect cu celelalte.

Un sistem MOP bun ar putea împărtăși robustețea internetului folosind componente care pot fi schimbate la cald în timp ce aplicația este în funcțiune. Acesta ar putea continua să funcționeze dacă utilizatorul se află pe un telefon mobil și se deconectează pentru că a intrat într-un tunel. Ar putea continua să funcționeze în cazul în care un uragan ar întrerupe alimentarea cu energie electrică a unuia dintre centrele de date în care se află serverele.

Este timpul ca lumea software-ului să renunțe la experimentul eșuat al moștenirii de clasă și să îmbrățișeze principiile matematice și științifice care au definit inițial spiritul OOP.

Este timpul să începem să construim software mai flexibil, mai rezistent și mai bine compus, cu MOP și programarea funcțională lucrând în armonie.

Nota: Acronimul MOP este deja folosit pentru a descrie „programarea orientată spre monitorizare” și este puțin probabil ca OOP să dispară în liniște.

Nu vă supărați dacă MOP nu se va impune ca jargon de programare.
Faceți MOP pentru OOP-urile dumneavoastră.

Învățați mai multe la EricElliottJS.com

Lecțiile video despre programarea funcțională sunt disponibile pentru membrii EricElliottJS.com. Dacă nu sunteți membru, înscrieți-vă astăzi.

.