Zapomenutá historie OOP
Poznámka: Jedná se o část seriálu „Skládání softwaru“ (nyní kniha!) o výuce funkcionálního programování a kompozičních softwarových technik v JavaScriptu ES6+ od základů. Zůstaňte s námi. Čeká vás toho mnohem víc!
Koupit knihu | Rejstřík | < Předchozí | Další >
Funkční a imperativní programovací paradigmata, která dnes používáme, byla poprvé matematicky zkoumána ve 30. letech 20. století pomocí lambda kalkulu a Turingova stroje, což jsou alternativní formulace univerzálního výpočtu (formalizované systémy, které mohou provádět obecné výpočty). Churchova Turingova věta ukázala, že lambda kalkul a Turingovy stroje jsou funkčně ekvivalentní – že cokoli, co lze vypočítat pomocí Turingova stroje, lze vypočítat pomocí lambda kalkulu a naopak.
Poznámka: Existuje rozšířená mylná představa, že Turingovy stroje mohou vypočítat cokoli, co lze vypočítat. Existují třídy problémů (např. problém zastavení), které mohou být pro některé případy vypočitatelné, ale nejsou obecně vypočitatelné pro všechny případy pomocí Turingových strojů. Když v tomto textu používám slovo „vypočitatelný“, mám na mysli „vypočitatelný Turingovým strojem“.
Lambda kalkulus představuje přístup k výpočtu shora dolů, aplikaci funkcí, zatímco formulace Turingova stroje s páskami/registry představuje přístup k výpočtu zdola nahoru, imperativní (postupný).
Jazyky nízké úrovně jako strojový kód a assembler se objevily ve 40. letech 20. století a koncem 50. let se objevily první populární jazyky vysoké úrovně. Dialekty jazyka Lisp se běžně používají dodnes, včetně jazyků Clojure, Scheme, AutoLISP atd. FORTRAN a COBOL se objevily v 50. letech 20. století a jsou příkladem imperativních jazyků vysoké úrovně, které se používají dodnes, ačkoli jazyky rodiny C nahradily COBOL i FORTRAN pro většinu aplikací.
Jak imperativní programování, tak funkcionální programování mají své kořeny v matematice teorie výpočtů, která předchází digitálním počítačům. Pojem „objektově orientované programování“ (OOP) vymyslel Alan Kay někdy v roce 1966 nebo 1967, když studoval na vysoké škole.
Prvotní inspirací pro OOP byla zásadní aplikace Sketchpad Ivana Sutherlanda. Byla vytvořena v letech 1961 až 1962 a publikována v jeho práci Sketchpad Thesis v roce 1963. Objekty byly datové struktury představující grafické obrázky zobrazené na obrazovce osciloskopu a obsahovaly dědičnost prostřednictvím dynamických delegátů, které Ivan Sutherland ve své práci nazval „mastery“. „Mistrem“ se mohl stát jakýkoli objekt a další instance objektů se nazývaly „výskyty“. Mastery ve Sketchpadu mají mnoho společného s prototypovou dědičností v JavaScriptu.
Poznámka: TX-2 v MIT Lincoln Laboratory byl jedním z prvních použití grafického počítačového monitoru využívajícího přímou interakci s obrazovkou pomocí světelného pera. EDSAC, který byl v provozu v letech 1948-1958, uměl zobrazovat grafiku na obrazovce. Whirlwind na MIT měl v roce 1949 funkční displej osciloskopu. Motivací projektu bylo vytvořit obecný letový simulátor schopný simulovat zpětnou vazbu přístrojů pro více letadel. To vedlo k vývoji výpočetního systému SAGE. TX-2 byl testovacím počítačem pro SAGE.
Prvním programovacím jazykem obecně uznávaným jako „objektově orientovaný“ byl Simula, specifikovaný v roce 1965. Stejně jako Sketchpad obsahovala Simula objekty a nakonec zavedla třídy, dědičnost tříd, podtřídy a virtuální metody.
Poznámka: Virtuální metoda je metoda definovaná ve třídě, která je určena k přepsání podtřídami. Virtuální metody umožňují programu volat metody, které v okamžiku kompilace kódu nemusí existovat, tím, že využívají dynamický dispečink k určení, jakou konkrétní metodu zavolat za běhu. JavaScript obsahuje dynamické typy a k určení, které metody mají být vyvolány, používá delegační řetězec, takže nemusí programátorům odhalovat koncept virtuálních metod. Jinak řečeno, všechny metody v JavaScriptu používají runtime method dispatch, takže metody v JavaScriptu nemusí být deklarovány jako „virtuální“, aby tuto vlastnost podporovaly.
„Vymyslel jsem termín „objektově orientovaný“ a mohu vám říct, že jsem neměl na mysli C++.“ ~ Alan Kay, OOPSLA ’97
Alan Kay vymyslel termín „objektově orientované programování“ na vysoké škole v roce 1966 nebo 1967. Hlavní myšlenkou bylo použití zapouzdřených minipočítačů v softwaru, které komunikují prostřednictvím předávání zpráv namísto přímého sdílení dat – aby se programy přestaly dělit na samostatné „datové struktury“ a „procedury“.
„Základní princip rekurzivního návrhu spočívá v tom, aby části měly stejnou sílu jako celek.“ ~ Bob Barton, hlavní konstruktér B5000, mainframu optimalizovaného pro běh v jazyce Algol-60.
Smalltalk vyvinuli Alan Kay, Dan Ingalls, Adele Goldberg a další v Xerox PARC. Smalltalk byl více objektově orientovaný než Simula – vše ve Smalltalku je objekt, včetně tříd, celých čísel a bloků (closures). Původní Smalltalk-72 neobsahoval podtřídy. To zavedl Dan Ingalls ve Smalltalku-76.
Ačkoli Smalltalk podporoval třídy a nakonec i podtřídy, Smalltalk nebyl o třídách a podtřídění věcí. Byl to funkcionální jazyk inspirovaný Lispem i Simulou. Alan Kay považuje zaměření průmyslu na podtřídy za odvádění pozornosti od skutečných výhod objektově orientovaného programování.
„Mrzí mě, že jsem pro toto téma kdysi dávno vymyslel termín „objekty“, protože díky němu se mnoho lidí soustředí na menší myšlenku. Velkou myšlenkou je zasílání zpráv.“
~ Alan Kay
V jedné e-mailové výměně z roku 2003 Alan Kay objasnil, co měl na mysli, když Smalltalk nazval „objektově orientovaným“:
„OOP pro mě znamená pouze zasílání zpráv, lokální uchovávání a ochranu a skrývání stavu-procesu a extrémní pozdní vázání všech věcí.“
~ Alan Kay
Jinými slovy, podle Alana Kaye jsou základními složkami OOP:
- Předávání zpráv
- Zapouzdření
- Dynamická vazba
Pozoruhodné je, že dědičnost a polymorfismus podtříd NEBYLY považovány za základní složky OOP Alanem Kayem, mužem, který tento termín vymyslel a přiblížil OOP masám.
Podstata OOP
Kombinace předávání zpráv a zapouzdření slouží k několika důležitým účelům:
- Zamezení sdílení proměnlivého stavu zapouzdřením stavu a izolací ostatních objektů od lokálních změn stavu. Jediný způsob, jak ovlivnit stav jiného objektu, je požádat (nikoliv přikázat) tento objekt o změnu stavu zasláním zprávy. Změny stavu jsou řízeny na lokální, buněčné úrovni, nikoli vystaveny sdílenému přístupu.
- Oddělení objektů od sebe navzájem – odesílatel zprávy je pouze volně svázán s příjemcem zprávy, a to prostřednictvím API pro zasílání zpráv.
- Přizpůsobivost a odolnost vůči změnám za běhu prostřednictvím pozdní vazby. Přizpůsobivost za běhu poskytuje mnoho velkých výhod, které Alan Kay považoval za zásadní pro OOP.
Tyto myšlenky byly inspirovány biologickými buňkami a/nebo jednotlivými počítači v síti prostřednictvím biologického vzdělání Alana Kaye a vlivem návrhu Arpanetu (rané verze internetu). I takto brzy si Alan Kay představoval software běžící na obřím distribuovaném počítači (internetu), kde jednotlivé počítače fungovaly jako biologické buňky, pracovaly nezávisle na svém izolovaném stavu a komunikovaly prostřednictvím předávání zpráv.
„Uvědomil jsem si, že metafora buňky/celého počítače by mě zbavila dat“
~ Alan Kay
Pod pojmem „zbavit se dat“ si Alan Kay jistě uvědomoval problémy se sdíleným proměnlivým stavem a těsnou vazbou způsobenou sdílenými daty – dnes běžná témata.
Na konci šedesátých let však byli programátoři agentury ARPA frustrováni nutností zvolit si reprezentaci datového modelu pro své programy ještě před sestavením softwaru. Procedury, které byly příliš těsně svázány s konkrétními datovými strukturami, nebyly odolné vůči změnám. Chtěli homogennější zacházení s daty.
“ smyslem OOP je nestarat se o to, co je uvnitř objektu. Objekty vytvořené na různých strojích a různými jazyky by měly být schopny spolu komunikovat “ ~ Alan Kay
Objekty mohou abstrahovat a skrývat implementace datových struktur. Vnitřní implementace objektu se může změnit, aniž by došlo k narušení ostatních částí softwarového systému. Ve skutečnosti by při extrémně pozdní vazbě mohl povinnosti objektu převzít úplně jiný počítačový systém a software by mohl fungovat dál. Objekty by mezitím mohly vystavit standardní rozhraní, které by pracovalo s jakoukoli datovou strukturou, kterou by objekt náhodou interně používal. Stejné rozhraní by mohlo pracovat se spojovým seznamem, stromem, proudem atd.
Alan Kay také viděl objekty jako algebraické struktury, které poskytují určité matematicky prokazatelné záruky o svém chování:
„Díky svému matematickému vzdělání jsem si uvědomil, že každý objekt by mohl mít několik algeber, které by s ním byly spojeny, a že by mohly existovat jejich rodiny, které by byly velmi velmi užitečné.“
~ Alan Kay
To se ukázalo jako pravdivé a tvoří to základ objektů, jako jsou sliby a čočky, které jsou inspirovány teorií kategorií.
Algebraická povaha objektů podle vize Alana Kaye by umožnila, aby objekty umožňovaly formální ověřování, deterministické chování a lepší testovatelnost, protože algebry jsou v podstatě operace, které se řídí několika pravidly ve formě rovnic.
V programátorském žargonu jsou algebry jako abstrakce tvořené funkcemi (operacemi) doprovázenými specifickými zákony vynucenými jednotkovými testy, kterými tyto funkce musí projít (axiomy/rovnice).
Tyto myšlenky byly po desetiletí ve většině OO jazyků rodiny C, včetně C++, Javy, C# atd., zapomenuty, ale začínají si znovu nacházet cestu do posledních verzí nejrozšířenějších OO jazyků.
Dalo by se říci, že svět programování znovu objevuje výhody funkcionálního programování a rozumného myšlení v kontextu OO jazyků.
Stejně jako předtím JavaScript a Smalltalk se většina moderních OO jazyků stává stále více „multiparadigmatickými jazyky“. Není důvod volit mezi funkcionálním programováním a OOP. Když se podíváme na historickou podstatu každého z nich, jsou to nejen kompatibilní, ale i doplňující se myšlenky.
Protože mají tolik společných rysů, rád říkám, že JavaScript je pomstou Smalltalku za nepochopení OOP ve světě. 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. Na typech .map()
nezáleží, protože se s nimi nepokouší manipulovat přímo, místo toho použije funkci, která očekává a vrací správné typy pro danou aplikaci.
// 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);
//
Tento obecný typový vztah je obtížné správně a důkladně vyjádřit v jazyce, jako je TypeScript, ale byl celkem snadno vyjádřitelný v Hindley Milnerových typech Haskellu s podporou vyšších kinded typů (typů typů).
Většina typových systémů byla příliš restriktivní na to, aby umožňovala volné vyjádření dynamických a funkcionálních myšlenek, jako je kompozice funkcí, volná kompozice objektů, rozšíření objektů za běhu, kombinátory, čočky atd. Jinými slovy, statické typy často ztěžují psaní komponovatelného softwaru.
Pokud je typový systém příliš restriktivní (např. TypeScript, Java), jste nuceni psát složitější kód, abyste dosáhli stejných cílů. To neznamená, že statické typy jsou špatný nápad nebo že všechny implementace statických typů jsou stejně restriktivní. S typovým systémem Haskellu jsem se setkal s mnohem menšími problémy.
Pokud jste příznivci statických typů a nevadí vám omezení, více síly pro vás, ale pokud vám některé rady v tomto textu připadají obtížné, protože je obtížné psát složené funkce a složené algebraické struktury, obviňujte typový systém, ne myšlenky. Lidé milují pohodlí svých SUV, ale nikdo si nestěžuje, že vás nenechají létat. K tomu potřebujete vozidlo s více stupni volnosti.
Pokud vám omezení zjednoduší kód, skvělé! Pokud vás ale omezení nutí psát složitější kód, možná jsou omezení špatná.
Co je to objekt?
Objekty v průběhu let zjevně získaly mnoho konotací. To, čemu v JavaScriptu říkáme „objekty“, jsou jednoduše složené datové typy, které nemají žádné důsledky ani z programování založeného na třídách, ani z předávání zpráv Alana Kaye.
V JavaScriptu tyto objekty mohou podporovat a často podporují zapouzdření, předávání zpráv, sdílení chování prostřednictvím metod, dokonce i polymorfismus podtříd (i když spíše pomocí řetězce delegování než typového odesílání). K libovolné vlastnosti můžete přiřadit libovolnou funkci. Můžete dynamicky vytvářet chování objektů a měnit význam objektu za běhu. JavaScript také podporuje zapouzdření pomocí uzávěrů pro zajištění soukromí při implementaci. To vše je však chování na základě volby.
Naše současná představa objektu je jednoduše složená datová struktura a nevyžaduje nic dalšího, aby byla považována za objekt. Programování pomocí těchto typů objektů však nečiní váš kód „objektově orientovaným“, stejně jako programování pomocí funkcí nečiní váš kód „funkcionálním“.
OOP už není skutečné OOP
Protože „objekt“ v moderních programovacích jazycích znamená mnohem méně, než znamenal pro Alana Kaye, používám pro popis pravidel skutečného OOP místo „objektu“ slovo „komponenta“. Mnoho objektů je v JavaScriptu vlastněno a manipulováno přímo jiným kódem, ale komponenty by měly zapouzdřovat a řídit svůj vlastní stav.
Real OOP znamená:
- Programování pomocí komponent (Alan Kayův „objekt“)
- Stav komponent musí být zapouzdřen
- Používání předávání zpráv pro komunikaci mezi objekty
- Komponenty lze přidávat/změňovat/nahrazovat za běhu
Většinu chování komponent lze specifikovat genericky pomocí algebraických datových struktur. Dědičnost zde není nutná. Komponenty mohou opakovaně používat chování ze sdílených funkcí a modulárních importů, aniž by sdílely svá data.
Manipulace s objekty nebo použití dědičnosti tříd v JavaScriptu neznamená, že „děláte OOP“. Používání komponent tímto způsobem ano. Ale populární použití je způsob, jakým se definují slova, takže bychom možná měli opustit OOP a místo „objektově orientované programování (OOP)“ tomu říkat „programování orientované na zprávy (MOP)“?
Je to náhoda, že se k uklízení nepořádku používají mopy?
Jak vypadá dobré MOP
Ve většině moderního softwaru existuje nějaké uživatelské rozhraní odpovědné za správu interakcí s uživatelem, nějaký kód spravující stav aplikace (uživatelská data) a kód spravující systémové nebo síťové vstupy/výstupy.
Každý z těchto systémů může vyžadovat dlouhodobé procesy, jako jsou posluchači událostí, stavy pro sledování věcí, jako je síťové připojení, stav prvků ui a stav samotné aplikace.
Dobrý MOP znamená, že namísto toho, aby se všechny tyto systémy vzájemně oslovovaly a přímo manipulovaly se svými stavy, systém komunikuje s ostatními komponentami prostřednictvím odesílání zpráv. Když uživatel klikne na tlačítko uložit, může být odeslána zpráva "SAVE"
, kterou může komponenta stavu aplikace interpretovat a předat obsluze aktualizace stavu (například funkci pure reducer). Možná, že po aktualizaci stavu by stavová komponenta mohla odeslat zprávu "STATE_UPDATED"
komponentě uživatelského rozhraní, která zase bude interpretovat stav, sladí, které části uživatelského rozhraní je třeba aktualizovat, a předá aktualizovaný stav dílčím komponentám, které tyto části uživatelského rozhraní obsluhují.
Komponenta síťového připojení by mezitím mohla monitorovat připojení uživatele k jinému počítači v síti, naslouchat zprávám a odesílat aktualizované reprezentace stavu pro uložení dat na vzdáleném počítači. Interně sleduje časovač srdečního tepu sítě, zda je připojení právě online nebo offline a podobně.
Tyto systémy nepotřebují znát podrobnosti o ostatních částech systému. Pouze o svých jednotlivých, modulárních záležitostech. Součásti systému jsou rozložitelné a rekomponovatelné. Implementují standardizovaná rozhraní, takže jsou schopny vzájemné spolupráce. Dokud je rozhraní splněno, můžete je nahradit náhradami, které mohou dělat stejnou věc různými způsoby, nebo úplně jiné věci se stejnými zprávami. Můžete tak učinit i za běhu a vše bude nadále správně fungovat.
Komponenty stejného softwarového systému nemusí být ani umístěny na stejném počítači. Systém by mohl být decentralizovaný. Síťové úložiště by mohlo rozdělit data do decentralizovaného úložného systému, jako je IPFS, takže uživatel by nebyl závislý na stavu konkrétního stroje, aby byla jeho data bezpečně zálohována a v bezpečí před hackery, kteří by je chtěli ukrást.
OOP byl částečně inspirován sítí Arpanet a jedním z cílů sítě Arpanet bylo vytvořit decentralizovanou síť, která by byla odolná proti útokům jako atomové bomby. Podle ředitele agentury DARPA v době vývoje sítě Arpanet Stephena J. Lukasika („Why the Arpanet Was Built“):
„Cílem bylo využít nové počítačové technologie pro potřeby vojenského velení a řízení proti jaderným hrozbám, dosáhnout přežitelného řízení jaderných sil USA a zlepšit vojenské taktické a řídicí rozhodování.“
Poznámka: Prvotním impulsem pro vznik sítě Arpanet bylo spíše pohodlí než jaderná hrozba a její zjevné obranné výhody se objevily až později. ARPA používala tři samostatné počítačové terminály pro komunikaci se třemi samostatnými počítačovými výzkumnými projekty. Bob Taylor chtěl jedinou počítačovou síť, která by propojila každý projekt s ostatními.
Dobrý systém MOP by mohl sdílet robustnost internetu pomocí komponent vyměnitelných za chodu aplikace. Mohl by pokračovat v práci, pokud uživatel používá mobilní telefon a přejde do režimu offline, protože vstoupil do tunelu. Mohl by fungovat i v případě, že hurikán vyřadí z provozu jedno z datových center, kde jsou umístěny servery.
Je načase, aby softwarový svět opustil neúspěšný experiment s dědičností tříd a přijal matematické a vědecké principy, které původně definovaly ducha OOP.
Je načase, abychom začali vytvářet flexibilnější, odolnější a lépe složený software, v němž budou MOP a funkcionální programování pracovat v harmonii.
Poznámka: Zkratka MOP se již používá k popisu „programování orientovaného na monitorování“ a je nepravděpodobné, že by OOP v tichosti zmizelo.
Nezlobte se, pokud se MOP jako programátorský žargon neuchytí.
Připravte si MOP pro své OOP.
Více se dozvíte na EricElliottJS.com
Video lekce o funkcionálním programování jsou k dispozici pro členy EricElliottJS.com. Pokud nejste členem, zaregistrujte se ještě dnes.