Programowanie oparte na prototypach - Prototype-based programming

Programowanie oparte na prototypach to styl programowania obiektowego, w którym ponowne wykorzystanie zachowania (znane jako dziedziczenie ) odbywa się poprzez proces ponownego wykorzystania istniejących obiektów, które służą jako prototypy . Ten model może być również znany jako programowanie prototypowe , zorientowane na prototypy, bezklasowe lub oparte na instancjach .

Programowanie oparte na prototypach wykorzystuje uogólnione obiekty, które można następnie klonować i rozszerzać. Używając owocu jako przykładu, obiekt „owoc” będzie reprezentował właściwości i funkcjonalność owoców w ogóle. Obiekt „banan” zostałby sklonowany z obiektu „owoc” i dodane zostałyby ogólne właściwości charakterystyczne dla bananów. Każdy pojedynczy obiekt „banan” zostałby sklonowany z ogólnego obiektu „banan”. Porównaj z paradygmatem klasowym , gdzie klasa „owoc” zostałaby rozszerzona o klasę „banan” .

Pierwszym językiem programowania zorientowanym na prototypy był Self , opracowany przez Davida Ungara i Randalla Smitha w połowie lat 80. XX wieku w celu badania zagadnień związanych z projektowaniem języków obiektowych. Od końca lat 90. paradygmat bezklasowy staje się coraz bardziej popularny. Niektóre obecne języki zorientowane są prototypowe JavaScript (i inne ECMAScript implementacje takich jak JScript i Flash „s ActionScript 1.0), Lua , Cecil , NewtonScript , Io , Ioke , MOO , REBOL i AHK .

Projektowanie i wdrażanie

Dziedziczenie prototypowe w JavaScript jest opisane przez Douglasa Crockforda jako:

Tworzysz obiekty prototypowe, a następnie… tworzysz nowe instancje. W JavaScript obiekty są mutowalne, więc możemy rozszerzać nowe instancje, nadając im nowe pola i metody. Mogą one następnie działać jako prototypy dla jeszcze nowszych obiektów. Nie potrzebujemy klas do tworzenia wielu podobnych obiektów… Obiekty dziedziczą po obiektach. Co może być bardziej zorientowane na obiekt niż to?

Zwolennicy programowania opartego na prototypach twierdzą, że zachęca ono programistę do skupienia się na zachowaniu pewnego zestawu przykładów, a dopiero później do martwienia się o klasyfikowanie tych obiektów do obiektów archetypowych, które są później używane w sposób podobny do klas . Wiele systemów opartych na prototypach zachęca do zmiany prototypów w czasie wykonywania , podczas gdy tylko kilka systemów zorientowanych obiektowo opartych na klasach (takich jak dynamiczny system zorientowany obiektowo, Common Lisp , Dylan , Objective-C , Perl , Python , Ruby lub Smalltalk ) pozwalają na zmianę klas podczas wykonywania programu.

Prawie wszystkie systemy oparte na prototypach są oparte na językach interpretowanych i dynamicznie typowanych . Jednak technicznie wykonalne są systemy oparte na statycznie typowanych językach. Przykładem takiego systemu jest język Omega omawiany w Prototype-Based Programming , chociaż według strony internetowej Omegi nawet Omega nie jest wyłącznie statyczna, ale raczej jej „kompilator może zdecydować się na użycie statycznego wiązania tam, gdzie jest to możliwe i może poprawić wydajność program."

Budowa obiektów

W językach opartych na prototypach nie ma klas jawnych. Obiekty dziedziczą bezpośrednio po innych obiektach poprzez właściwość prototypu. Właściwość prototype jest wywoływana prototypew Self i JavaScript lub protow Io . Istnieją dwie metody konstruowania nowych obiektów: tworzenie obiektów ex nihilo ("z niczego") lub poprzez klonowanie istniejącego obiektu. Ten pierwszy jest obsługiwany przez pewną formę literału obiektowego , deklaracje, w których obiekty mogą być definiowane w czasie wykonywania za pomocą specjalnej składni, takiej jak {...}i przekazywane bezpośrednio do zmiennej. Podczas gdy większość systemów obsługuje różne rodzaje klonowania, tworzenie obiektów ex nihilo nie jest tak widoczne.

W językach opartych na klasach nowa instancja jest konstruowana za pomocą funkcji konstruktora klasy , specjalnej funkcji, która rezerwuje blok pamięci dla elementów członkowskich obiektu (właściwości i metod) i zwraca odwołanie do tego bloku. Opcjonalny zestaw argumentów konstruktora można przekazać do funkcji i zwykle są one przechowywane we właściwościach. Powstała instancja odziedziczy wszystkie metody i właściwości, które zostały zdefiniowane w klasie, która działa jak rodzaj szablonu, z którego można konstruować obiekty o podobnym typie.

Systemy obsługujące tworzenie obiektów ex nihilo umożliwiają tworzenie nowych obiektów od podstaw bez klonowania z istniejącego prototypu. Takie systemy zapewniają specjalną składnię do określania właściwości i zachowań nowych obiektów bez odwoływania się do istniejących obiektów. W wielu językach prototypowych istnieje obiekt główny, często nazywany Object , który jest ustawiony jako domyślny prototyp dla wszystkich innych obiektów tworzonych w czasie wykonywania i który zawiera powszechnie potrzebne metody, takie jak toString()funkcja zwracająca opis obiektu w postaci łańcucha . Jeden użyteczny aspekt ex nihilo tworzenia obiektów jest zapewnienie, że gniazdo jest nowy obiekt (właściwości i metody) nazwy nie mają przestrzeni nazw konfliktów z najwyższego poziomu obiektów obiektu. (W języku JavaScript można to zrobić za pomocą prototypu null, tj Object.create(null). .)

Klonowanie odnosi się do procesu, w którym nowy obiekt jest konstruowany przez kopiowanie zachowania istniejącego obiektu (jego prototypu). Nowy przedmiot ma wtedy wszystkie cechy oryginału. Od tego momentu nowy obiekt można modyfikować. W niektórych systemach wynikowy obiekt potomny utrzymuje wyraźne łącze (poprzez delegację lub podobieństwo ) do swojego prototypu, a zmiany w prototypie powodują, że odpowiednie zmiany są widoczne w jego klonie. Inne systemy, takie jak język programowania podobny do Forth Kevo , nie propagują zmian z prototypu w ten sposób i zamiast tego stosują bardziej konkatenacyjny model, w którym zmiany w sklonowanych obiektach nie są automatycznie propagowane przez potomków.

// Example of true prototypal inheritance style 
// in JavaScript.

// object creation using the literal 
// object notation {}.
const foo = { name: "foo", one: 1, two: 2 };

// Another object.
const bar = { two: "two", three: 3 };

// Object.setPrototypeOf() is a method introduced in ECMAScript 2015.
// For the sake of simplicity, let us pretend 
// that the following line works regardless of the 
// engine used:
Object.setPrototypeOf(bar, foo); // foo is now the prototype of bar.

// If we try to access foo's properties from bar 
// from now on, we'll succeed. 
bar.one; // Resolves to 1.

// The child object's properties are also accessible.
bar.three; // Resolves to 3.

// Own properties shadow prototype properties
bar.two; // Resolves to "two"
bar.name; // unaffected, resolves to "foo"
foo.name; // Resolves to "foo"

Na inny przykład:

const foo = { one: 1, two: 2 };

// bar.[[prototype]] = foo
const bar = Object.create(foo);

bar.three = 3;

bar.one; // 1
bar.two; // 2
bar.three; // 3

Delegacja

W językach opartych na prototypach, które używają delegowania , środowisko uruchomieniowe języka jest w stanie wysłać poprawną metodę lub znaleźć odpowiedni fragment danych, po prostu podążając za serią wskaźników delegacji (od obiektu do jego prototypu), aż do znalezienia dopasowania. Wszystko, co jest wymagane do ustanowienia tego współdzielenia zachowania między obiektami, to wskaźnik delegacji. W przeciwieństwie do relacji między klasą a instancją w językach obiektowych opartych na klasach, relacja między prototypem a jego odgałęzieniami nie wymaga, aby obiekt potomny miał pamięć lub podobieństwo strukturalne do prototypu poza tym łączem. Jako taki, obiekt potomny może być modyfikowany i poprawiany w czasie bez zmiany struktury powiązanego z nim prototypu, jak w systemach opartych na klasach. Ważne jest również, aby pamiętać, że nie tylko dane, ale także metody mogą być dodawane lub zmieniane. Z tego powodu niektóre języki oparte na prototypach odnoszą się zarówno do danych, jak i metod jako „slotów” lub „członków”.

Powiązanie

W prototypowaniu konkatenatywnym - podejściu zaimplementowanym przez język programowania Kevo - nie ma widocznych wskaźników ani linków do oryginalnego prototypu, z którego klonowany jest obiekt. Obiekt prototypowy (nadrzędny) jest kopiowany, a nie połączony i nie ma delegowania. W rezultacie zmiany w prototypie nie zostaną odzwierciedlone w sklonowanych obiektach.

Główna różnica koncepcyjna w tym układzie polega na tym, że zmiany wprowadzone do obiektu prototypowego nie są automatycznie przenoszone na klony. Może to być postrzegane jako zaleta lub wada. (Jednak Kevo zapewnia dodatkowe prymitywy do publikowania zmian w zestawach obiektów na podstawie ich podobieństwa — tak zwanych podobieństw rodzinnych lub mechanizmu rodziny klonów — a nie poprzez pochodzenie taksonomiczne, co jest typowe w modelu delegowania). że prototypowanie oparte na delegowaniu ma dodatkową wadę polegającą na tym, że zmiany w obiekcie podrzędnym mogą wpływać na późniejsze działanie obiektu nadrzędnego. Jednak ten problem nie jest nieodłączny od modelu opartego na delegowaniu i nie występuje w językach opartych na delegowaniu, takich jak JavaScript, które zapewniają, że zmiany w obiekcie podrzędnym są zawsze rejestrowane w samym obiekcie podrzędnym, a nigdy w rodzicach (tj. value zaciemnia wartość rodzica, a nie zmienia wartość rodzica).

W uproszczonych implementacjach, konkatenatywne prototypowanie będzie miało szybsze wyszukiwanie elementów członkowskich niż prototypowanie oparte na delegowaniu (ponieważ nie ma potrzeby podążania za łańcuchem obiektów nadrzędnych), ale odwrotnie będzie używało więcej pamięci (ponieważ wszystkie gniazda są kopiowane, a nie jeden gniazdo wskazujące na obiekt nadrzędny). Bardziej wyrafinowane implementacje mogą jednak uniknąć tego problemu, chociaż wymagane są kompromisy między szybkością a pamięcią. Na przykład systemy z konkatenacyjnym prototypowaniem mogą wykorzystywać implementację kopiowania przy zapisie, aby umożliwić zakulisowe udostępnianie danych — i takie podejście jest rzeczywiście stosowane przez Kevo. I odwrotnie, systemy z prototypowaniem opartym na delegowaniu mogą używać buforowania, aby przyspieszyć wyszukiwanie danych.

Krytyka

Zwolennicy modeli obiektowych opartych na klasach, którzy krytykują systemy oparte na prototypach, często mają obawy podobne do obaw, jakie mają zwolennicy systemów typu statycznego dla języków programowania z systemami typu dynamicznego (patrz datatype ). Zazwyczaj takie obawy dotyczą: poprawności , bezpieczeństwa , przewidywalności , wydajności i nieznajomości programisty.

W pierwszych trzech punktach klasy są często postrzegane jako analogiczne do typów (w większości statycznie typizowanych języków obiektowych pełnią tę rolę) i proponuje się, aby zapewniały umowne gwarancje ich instancjom oraz użytkownikom ich instancji, że będą się zachowywać. w pewien określony sposób.

Jeśli chodzi o wydajność, deklarowanie klas upraszcza wiele optymalizacji kompilatora, które umożliwiają opracowywanie wydajnych metod i wyszukiwania zmiennych instancji. W przypadku języka Self dużo czasu poświęcono na opracowywanie, kompilowanie i interpretowanie technik poprawiających wydajność systemów opartych na prototypach w porównaniu z systemami opartymi na klasach.

Powszechną krytyką pod adresem języków opartych na prototypach jest to, że społeczność twórców oprogramowania nie jest z nimi zaznajomiona, pomimo popularności i przenikania rynku przez JavaScript . Ten poziom wiedzy na temat systemów opartych na prototypach wydaje się rosnąć wraz z rozpowszechnianiem się frameworków JavaScript i złożonym wykorzystaniem JavaScript w miarę dojrzewania sieci . ECMAScript 6 wprowadził klasy jako cukier składniowy w stosunku do istniejącego dziedziczenia opartego na prototypach JavaScript, zapewniając alternatywny sposób tworzenia obiektów i radzenia sobie z dziedziczeniem.

Języki wspierające programowanie prototypowe

Zobacz też

Bibliografia

Dalsza lektura