Zapomniana historia OOP

Uwaga: To jest część serii „Composing Software” (teraz książka!) o nauce programowania funkcyjnego i technik kompozycji oprogramowania w JavaScript ES6+ od podstaw. Bądźcie czujni. Będzie tego o wiele więcej!
Kup książkę | Indeks | < Poprzedni | Następny >

Paradygmaty programowania funkcyjnego i imperatywnego, których używamy dzisiaj, zostały po raz pierwszy zbadane matematycznie w latach 30. ubiegłego wieku za pomocą rachunku lambda i maszyny Turinga, które są alternatywnymi sformułowaniami uniwersalnych obliczeń (sformalizowanych systemów, które mogą wykonywać ogólne obliczenia). Teza Churcha Turinga pokazała, że rachunek lambda i maszyny Turinga są funkcjonalnie równoważne – że wszystko, co może być obliczone przy użyciu maszyny Turinga, może być obliczone przy użyciu rachunku lambda i vice versa.

Uwaga: Istnieje powszechne błędne przekonanie, że maszyny Turinga mogą obliczyć wszystko, co jest obliczalne. Istnieją klasy problemów (np. problem haltingu), które mogą być obliczalne dla niektórych przypadków, ale nie są ogólnie obliczalne dla wszystkich przypadków przy użyciu maszyn Turinga. Kiedy używam słowa „obliczalny” w tym tekście, mam na myśli „obliczalny przez maszynę Turinga”.

Kalkulator lambda reprezentuje podejście do obliczeń z góry na dół, z zastosowaniem funkcji, podczas gdy sformułowanie maszyny Turinga z taśmą klejącą/rejestrem reprezentuje podejście do obliczeń z dołu do góry, imperatywne (krok po kroku).

Języki niskiego poziomu, takie jak kod maszynowy i asembler, pojawiły się w latach czterdziestych, a pod koniec lat pięćdziesiątych pojawiły się pierwsze popularne języki wysokiego poziomu. Dialekty Lispa są nadal w powszechnym użyciu dzisiaj, w tym Clojure, Scheme, AutoLISP itp. FORTRAN i COBOL pojawiły się w latach 50. i są przykładami imperatywnych języków wysokiego poziomu, które są nadal używane, chociaż języki z rodziny C zastąpiły zarówno COBOL, jak i FORTRAN w większości zastosowań.

Oba imperatywne programowanie i programowanie funkcjonalne mają swoje korzenie w matematyce teorii obliczeń, poprzedzając komputery cyfrowe. Termin „programowanie obiektowe” (OOP) został ukuty przez Alana Kaya około 1966 lub 1967 roku, gdy był on w szkole średniej.

Pierwotna aplikacja Sketchpad Ivana Sutherlanda była wczesną inspiracją dla OOP. Powstała ona w latach 1961-1962 i została opublikowana w jego Sketchpad Thesis w 1963 roku. Obiekty były strukturami danych reprezentującymi graficzne obrazy wyświetlane na ekranie oscyloskopu i charakteryzowały się dziedziczeniem poprzez dynamicznych delegatów, których Ivan Sutherland nazywał w swojej pracy „mistrzami”. Każdy obiekt mógł stać się „masterem”, a dodatkowe instancje obiektów były nazywane „wystąpieniami”. Mistrzowie Sketchpada mają wiele wspólnego z prototypowym dziedziczeniem w JavaScript.

Uwaga: TX-2 w MIT Lincoln Laboratory był jednym z wczesnych zastosowań graficznego monitora komputerowego wykorzystującego bezpośrednią interakcję z ekranem za pomocą pióra. EDSAC, który działał w latach 1948-1958 mógł wyświetlać grafikę na ekranie. Whirlwind na MIT miał działający wyświetlacz oscyloskopowy w 1949 roku. Motywacją projektu było stworzenie ogólnego symulatora lotu, który byłby w stanie symulować sprzężenie zwrotne z przyrządami dla wielu samolotów. Doprowadziło to do opracowania systemu obliczeniowego SAGE. TX-2 był komputerem testowym dla SAGE.

Pierwszym językiem programowania powszechnie uznawanym za „obiektowy” była Simula, opracowana w 1965 roku. Podobnie jak Sketchpad, Simula zawierała obiekty i ostatecznie wprowadziła klasy, dziedziczenie klas, podklasy i metody wirtualne.

Uwaga: Metoda wirtualna to metoda zdefiniowana w klasie, która jest przeznaczona do nadpisywania przez podklasy. Metody wirtualne pozwalają programowi na wywoływanie metod, które mogą nie istnieć w momencie kompilacji kodu, poprzez zastosowanie dynamicznej wysyłki, aby określić, jaką konkretną metodę wywołać w czasie wykonywania. JavaScript posiada dynamiczne typy i używa łańcucha delegacji do określenia, które metody wywołać, więc nie musi ujawniać pojęcia metod wirtualnych programistom. Mówiąc inaczej, wszystkie metody w JavaScript używają wysyłki metod w czasie biegu, więc metody w JavaScript nie muszą być zadeklarowane jako „wirtualne”, aby wspierać tę cechę.

„Wymyśliłem termin 'zorientowany obiektowo’ i mogę powiedzieć, że nie miałem na myśli C++.” ~ Alan Kay, OOPSLA ’97

Alan Kay ukuł termin „programowanie obiektowe” w szkole średniej w 1966 lub 1967 roku. Wielką ideą było użycie hermetyzowanych minikomputerów w oprogramowaniu, które komunikowały się poprzez przekazywanie komunikatów, a nie bezpośrednie udostępnianie danych – aby przestać rozbijać programy na oddzielne „struktury danych” i „procedury”.

„Podstawową zasadą projektowania rekurencyjnego jest sprawienie, aby części miały taką samą moc jak całość.” ~ Bob Barton, główny projektant B5000, mainframe’a zoptymalizowanego do uruchamiania Algolu-60.

Smalltalk został opracowany przez Alana Kaya, Dana Ingallsa, Adele Goldberg i innych w Xerox PARC. Smalltalk był bardziej zorientowany obiektowo niż Simula – wszystko w Smalltalku jest obiektem, włączając w to klasy, liczby całkowite i bloki (domknięcia). Oryginalny Smalltalk-72 nie posiadał podklas. Zostało ono wprowadzone w Smalltalk-76 przez Dana Ingallsa.

Pomimo, że Smalltalk wspierał klasy i ostatecznie podklasy, nie chodziło w nim o klasy czy podklasowanie rzeczy. Był to język funkcjonalny, inspirowany Lispem i Simulą. Alan Kay uważa, że koncentracja branży na podklasach odwraca uwagę od prawdziwych korzyści płynących z programowania obiektowego.

„Przykro mi, że dawno temu ukułem termin „obiekty” dla tego tematu, ponieważ sprawia on, że wielu ludzi skupia się na mniej istotnej idei. Wielką ideą jest przekazywanie wiadomości.”
~ Alan Kay

W wymianie e-maili z 2003 r. Alan Kay wyjaśnił, co miał na myśli, gdy nazwał Smalltalk „zorientowanym obiektowo”:

„OOP dla mnie oznacza tylko przekazywanie wiadomości, lokalne przechowywanie i ochronę oraz ukrywanie stanu-procesu, a także ekstremalne późne wiązanie wszystkich rzeczy.”
~ Alan Kay

Innymi słowy, według Alana Kaya, podstawowymi składnikami OOP są:

  • Przekazywanie komunikatów
  • Ekapsulacja
  • Wiązanie dynamiczne

W szczególności, dziedziczenie i polimorfizm podklas NIE zostały uznane za istotne składniki OOP przez Alana Kaya, człowieka, który ukuł termin i przyniósł OOP masom.

Esencja OOP

Kombinacja przekazywania komunikatów i enkapsulacji służy kilku ważnym celom:

  • Unikanie współdzielenia mutowalnego stanu poprzez enkapsulację stanu i izolowanie innych obiektów od lokalnych zmian stanu. Jedynym sposobem na wpłynięcie na stan innego obiektu jest poproszenie (nie polecenie) tego obiektu o jego zmianę poprzez wysłanie komunikatu. Zmiany stanu są kontrolowane na poziomie lokalnym, komórkowym, a nie narażone na współdzielony dostęp.
  • Oddzielenie obiektów od siebie – nadawca komunikatu jest tylko luźno powiązany z odbiorcą komunikatu, poprzez API komunikatów.
  • Adaptacyjność i odporność na zmiany w czasie działania poprzez późne wiązanie. Możliwość adaptacji w czasie działania zapewnia wiele wspaniałych korzyści, które Alan Kay uważał za niezbędne dla OOP.

Pomysły te zostały zainspirowane komórkami biologicznymi i/lub pojedynczymi komputerami w sieci dzięki wykształceniu Alana Kaya w dziedzinie biologii oraz wpływowi projektu Arpanet (wczesna wersja Internetu). Nawet tak wcześnie Alan Kay wyobrażał sobie oprogramowanie działające na gigantycznym, rozproszonym komputerze (Internecie), gdzie poszczególne komputery działały jak komórki biologiczne, działając niezależnie na własnym, odizolowanym terenie i komunikując się poprzez przekazywanie wiadomości.

„Zdałem sobie sprawę, że metafora komórki/komputera w całości pozbędzie się danych”
~ Alan Kay

Przez „pozbycie się danych” Alan Kay był z pewnością świadomy problemów ze współdzielonym, mutowalnym stanem i ścisłym sprzężeniem spowodowanym przez współdzielone dane – powszechne tematy w dzisiejszych czasach.

Ale w późnych latach 60. programiści ARPA byli sfrustrowani koniecznością wyboru reprezentacji modelu danych dla swoich programów przed budową oprogramowania. Procedury, które były zbyt mocno związane z konkretnymi strukturami danych, nie były odporne na zmiany. Chcieli bardziej jednorodnego traktowania danych.

” cały sens OOP polega na tym, aby nie musieć się martwić o to, co jest wewnątrz obiektu. Obiekty wykonane na różnych maszynach i przy użyciu różnych języków powinny być w stanie rozmawiać ze sobą ” ~ Alan Kay

Obiekty mogą abstrahować i ukrywać implementacje struktur danych. Wewnętrzna implementacja obiektu może się zmienić bez łamania innych części systemu oprogramowania. W rzeczywistości, z ekstremalnie późnym wiązaniem, zupełnie inny system komputerowy mógłby przejąć obowiązki obiektu, a oprogramowanie mogłoby nadal działać. Obiekty, w międzyczasie, mogłyby wyeksponować standardowy interfejs, który działa z jakąkolwiek strukturą danych, której obiekt używałby wewnętrznie. Ten sam interfejs mógłby działać z listą połączoną, drzewem, strumieniem i tak dalej.

Alan Kay postrzegał również obiekty jako struktury algebraiczne, które dają pewne matematycznie sprawdzalne gwarancje dotyczące ich zachowania:

„Moje matematyczne wykształcenie uświadomiło mi, że każdy obiekt może mieć kilka algebr z nim związanych, że mogą istnieć ich rodziny i że będą one bardzo użyteczne.”
~ Alan Kay

To okazało się prawdą i stanowi podstawę dla obiektów takich jak obietnice i soczewki, oba zainspirowane teorią kategorii.

Algebraiczna natura wizji obiektów Alana Kaya pozwoliłaby obiektom na formalną weryfikację, deterministyczne zachowanie i lepszą testowalność, ponieważ algebry są w istocie operacjami, które przestrzegają kilku reguł w formie równań.

W języku programistów, algebry są jak abstrakcje złożone z funkcji (operacji), którym towarzyszą specyficzne prawa wymuszone przez testy jednostkowe, które te funkcje muszą przejść (aksjomaty/równania).

Pomysły te zostały zapomniane na dekady w większości języków OO z rodziny C, włączając w to C++, Javę, C#, itd, ale zaczynają znajdować drogę powrotną do ostatnich wersji większości powszechnie używanych języków OO.

Można powiedzieć, że świat programowania odkrywa na nowo korzyści płynące z programowania funkcyjnego i uzasadnionego myślenia w kontekście języków OO.

Jak JavaScript i Smalltalk przed nim, większość nowoczesnych języków OO staje się coraz bardziej „językami wieloparadygmatowymi”. Nie ma powodu, aby wybierać między programowaniem funkcjonalnym a OOP. Kiedy patrzymy na historyczną istotę każdego z nich, są one nie tylko kompatybilne, ale uzupełniające się idee.

Ponieważ mają tak wiele cech wspólnych, lubię mówić, że JavaScript jest zemstą Smalltalka na światowym niezrozumieniu 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. Typy nie mają znaczenia dla .map() ponieważ nie próbuje manipulować nimi bezpośrednio, zamiast tego stosując funkcję, która oczekuje i zwraca prawidłowe typy dla aplikacji.

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

Ta generyczna relacja typu jest trudna do wyrażenia poprawnie i dokładnie w języku takim jak TypeScript, ale była dość łatwa do wyrażenia w typach Hindleya Milnera Haskella z obsługą wyższych typów kinded (typy typów).

Większość systemów typów była zbyt restrykcyjna, aby umożliwić swobodne wyrażanie dynamicznych i funkcjonalnych pomysłów, takich jak kompozycja funkcji, wolna kompozycja obiektów, rozszerzanie obiektów runtime, kombinatory, soczewki itp. Innymi słowy, statyczne typy często utrudniają pisanie złożonego oprogramowania.

Jeśli twój system typów jest zbyt restrykcyjny (np. TypeScript, Java), jesteś zmuszony do pisania bardziej zagmatwanego kodu, aby osiągnąć te same cele. Nie oznacza to, że typy statyczne są złym pomysłem, lub że wszystkie implementacje typów statycznych są równie restrykcyjne. Napotkałem znacznie mniej problemów z systemem typów Haskella.

Jeśli jesteś fanem statycznych typów i nie przeszkadzają ci ograniczenia, więcej mocy dla ciebie, ale jeśli uważasz, że niektóre z porad w tym tekście są trudne, ponieważ trudno jest wpisać złożone funkcje i złożone struktury algebraiczne, obwiniaj system typów, a nie pomysły. Ludzie kochają komfort swoich SUV-ów, ale nikt nie narzeka, że nie pozwalają one latać. Do tego potrzebny jest pojazd z większą liczbą stopni swobody.

Jeśli ograniczenia czynią twój kod prostszym, świetnie! Ale jeśli ograniczenia zmuszają Cię do pisania bardziej skomplikowanego kodu, być może ograniczenia są złe.

Co to jest obiekt?

Obiekty wyraźnie nabrały wielu konotacji na przestrzeni lat. To co nazywamy „obiektami” w JavaScript jest po prostu złożonymi typami danych, bez żadnych implikacji wynikających z programowania opartego na klasach lub przekazywania wiadomości Alana Kaya.

W JavaScript, te obiekty mogą i często wspierają enkapsulację, przekazywanie wiadomości, dzielenie się zachowaniem poprzez metody, nawet polimorfizm podklas (aczkolwiek używając łańcucha delegacji zamiast wysyłki opartej na typie). Możesz przypisać dowolną funkcję do dowolnej właściwości. Można dynamicznie budować zachowania obiektów i zmieniać ich znaczenie w czasie wykonywania. JavaScript wspiera również enkapsulację przy użyciu domknięć dla prywatności implementacji. Ale wszystko to jest zachowaniem opt-in.

Nasze obecne wyobrażenie obiektu jest po prostu złożoną strukturą danych, i nie wymaga niczego więcej, aby być uważanym za obiekt. Ale programowanie z użyciem tego rodzaju obiektów nie czyni twojego kodu „obiektowym” tak samo jak programowanie z użyciem funkcji nie czyni twojego kodu „funkcjonalnym”.

OOP nie jest już prawdziwym OOP

Ponieważ „obiekt” we współczesnych językach programowania znaczy znacznie mniej niż dla Alana Kaya, używam „komponentu” zamiast „obiektu” do opisania zasad prawdziwego OOP. Wiele obiektów jest własnością i jest manipulowanych bezpośrednio przez inny kod w JavaScript, ale komponenty powinny enkapsulować i kontrolować swój własny stan.

Prawdziwy OOP oznacza:

  • Programowanie z komponentami („obiekt” Alana Kaya)
  • Stan komponentu musi być enkapsulowany
  • Używanie przekazywania komunikatów do komunikacji między obiektami
  • Komponenty mogą być dodawane/zmieniane/zastępowane w czasie działania

Większość zachowań komponentów może być określona generycznie przy użyciu algebraicznych struktur danych. Dziedziczenie nie jest tu potrzebne. Komponenty mogą ponownie wykorzystywać zachowania ze współdzielonych funkcji i modułowego importu bez dzielenia się swoimi danymi.

Manipulowanie obiektami lub używanie dziedziczenia klas w JavaScript nie oznacza, że „robisz OOP”. Używanie komponentów w ten sposób już tak. Ale popularne użycie jest jak słowa są definiowane, więc może powinniśmy porzucić OOP i nazwać to „Message Oriented Programming (MOP)” zamiast „Object Oriented Programming (OOP)”?

Czy to przypadek, że mopy są używane do sprzątania bałaganu?

What Good MOP Looks Like

W większości nowoczesnego oprogramowania, jest jakiś UI odpowiedzialny za zarządzanie interakcjami użytkownika, jakiś kod zarządzający stanem aplikacji (dane użytkownika), i kod zarządzający systemem lub siecią I/O.

Każdy z tych systemów może wymagać długotrwałych procesów, takich jak nasłuchiwacze zdarzeń, stanów, aby śledzić takie rzeczy jak połączenie sieciowe, status elementów UI i sam stan aplikacji.

Dobry MOP oznacza, że zamiast wszystkich tych systemów sięgać i bezpośrednio manipulować stanem każdego z nich, system komunikuje się z innymi komponentami poprzez wysyłanie komunikatów. Kiedy użytkownik kliknie na przycisk zapisu, może zostać wysłana wiadomość "SAVE", którą komponent stanowy aplikacji może zinterpretować i przekazać do handler’a aktualizacji stanu (takiego jak czysta funkcja reduktora). Być może po zaktualizowaniu stanu, komponent stanowy może wysłać wiadomość "STATE_UPDATED" do komponentu UI, który z kolei zinterpretuje stan, uzgodni, które części UI wymagają aktualizacji i przekaże zaktualizowany stan do podkomponentów, które obsługują te części UI.

W międzyczasie, komponent połączenia sieciowego może monitorować połączenie użytkownika z inną maszyną w sieci, nasłuchując wiadomości i wysyłając zaktualizowane reprezentacje stanu, aby zapisać dane na zdalnej maszynie. Wewnętrznie śledzi on timer bicia serca sieci, czy połączenie jest aktualnie online czy offline, i tak dalej.

Systemy te nie muszą wiedzieć o szczegółach innych części systemu. Tylko o ich indywidualnych, modułowych problemach. Komponenty systemu są dekomponowalne i rekompozycjonowalne. Implementują one standardowe interfejsy tak, że są w stanie współdziałać. Tak długo, jak interfejs jest spełniony, możesz zastąpić zamienniki, które mogą robić to samo na różne sposoby lub zupełnie inne rzeczy z tymi samymi wiadomościami. Możesz nawet zrobić to w czasie działania, a wszystko będzie działać poprawnie.

Komponenty tego samego systemu oprogramowania nie muszą nawet znajdować się na tej samej maszynie. System może być zdecentralizowany. Pamięć sieciowa może rozdzielać dane na zdecentralizowane systemy pamięci masowej, takie jak IPFS, dzięki czemu użytkownik nie jest zależny od stanu zdrowia konkretnej maszyny, aby zapewnić, że jego dane są bezpiecznie przechowywane w kopii zapasowej i bezpieczne przed hakerami, którzy mogą chcieć je ukraść.

OOP został częściowo zainspirowany przez Arpanet, a jednym z celów Arpanetu było zbudowanie zdecentralizowanej sieci, która mogłaby być odporna na ataki takie jak bomby atomowe. Według dyrektora DARPA podczas rozwoju Arpanetu, Stephena J. Lukasika („Why the Arpanet Was Built”):

„Celem było wykorzystanie nowych technologii komputerowych do zaspokojenia potrzeb wojskowego dowodzenia i kontroli przed zagrożeniami nuklearnymi, osiągnięcia przetrwałej kontroli amerykańskich sił nuklearnych oraz poprawy wojskowych decyzji taktycznych i zarządczych.”

Uwaga: Głównym impulsem do stworzenia Arpanetu była wygoda, a nie zagrożenie nuklearne, a jego oczywiste zalety obronne pojawiły się później. ARPA używała trzech oddzielnych terminali komputerowych do komunikacji z trzema oddzielnymi komputerowymi projektami badawczymi. Bob Taylor chciał, aby jedna sieć komputerowa łączyła każdy projekt z pozostałymi.

Dobry system MOP może dzielić odporność Internetu, wykorzystując komponenty, które można wymieniać na gorąco podczas pracy aplikacji. Mógłby kontynuować pracę, jeśli użytkownik jest na telefonie komórkowym, a oni iść offline, ponieważ weszli do tunelu. Mogłaby nadal działać, gdyby huragan wyłączył zasilanie w jednym z centrów danych, gdzie znajdują się serwery.

Czas, aby świat oprogramowania porzucił nieudany eksperyment dziedziczenia klasowego i przyjął zasady matematyki i nauki, które pierwotnie zdefiniowały ducha OOP.

Czas, abyśmy zaczęli budować bardziej elastyczne, bardziej odporne, lepiej skomponowane oprogramowanie, z MOP i programowaniem funkcjonalnym pracującym w harmonii.

Uwaga: Akronim MOP jest już używany do opisania „programowania zorientowanego na monitorowanie” i jest mało prawdopodobne, że OOP odejdzie po cichu.

Nie denerwuj się, jeśli MOP nie przyjmie się jako lingo programistyczne.
Do MOP up your OOPs.

Learn More at EricElliottJS.com

Lekcje wideo na temat programowania funkcyjnego są dostępne dla członków EricElliottJS.com. Jeśli nie jesteś członkiem, zarejestruj się już dziś.