Późne wiązanie obiektowe

11

W Alan Kays Definition Object Oriented znajduje się definicja, której częściowo nie rozumiem:

OOP oznacza dla mnie tylko wysyłanie wiadomości, lokalne przechowywanie, ochronę i ukrywanie procesów państwowych oraz ekstremalne późne wiązanie wszystkich rzeczy.

Ale co oznacza „LateBinding”? Jak mogę to zastosować w języku takim jak C #? I dlaczego to takie ważne?

Luca Zulian
źródło
2
OOP w C # prawdopodobnie nie jest tym, o którym myślał Alan Kay.
Doc Brown
Zgadzam się z tobą, absolutnie ... przykłady są mile widziane we wszystkich językach
Luca Zulian

Odpowiedzi:

14

„Powiązanie” odnosi się do rozstrzygnięcia nazwy metody na fragment kodu możliwego do wywołania. Zwykle wywołanie funkcji można rozwiązać w czasie kompilacji lub w czasie łącza. Przykładem języka używającego wiązania statycznego jest C:

int foo(int x);

int main(int, char**) {
  printf("%d\n", foo(40));
  return 0;
}

int foo(int x) { return x + 2; }

W tym foo(40)przypadku kompilator może rozwiązać połączenie. To wczesne pozwala na pewne optymalizacje, takie jak wstawianie. Najważniejsze zalety to:

  • możemy sprawdzić typ
  • możemy optymalizować

Z drugiej strony niektóre języki odraczają rozstrzyganie funkcji do ostatniej możliwej chwili. Przykładem jest Python, w którym możemy na nowo zdefiniować symbole w locie:

def foo():
    """"call the bar() function. We have no idea what bar is."""
    return bar()

def bar():
    return 42

print(foo()) # bar() is 42, so this prints "42"

# use reflection to overwrite the "bar" variable
locals()["bar"] = lambda: "Hello World"

print(foo()) # bar() was redefined to "Hello World", so it prints that

bar = 42
print(foo()) # throws TypeError: 'int' object is not callable

To jest przykład późnego wiązania. Chociaż sprawia, że ​​rygorystyczne sprawdzanie typów jest nieuzasadnione (sprawdzanie typów może być wykonywane tylko w czasie wykonywania), jest jednak znacznie bardziej elastyczne i pozwala nam wyrażać pojęcia, których nie można wyrazić w ramach statycznego pisania lub wczesnego wiązania. Na przykład możemy dodać nowe funkcje w czasie wykonywania.

Wysyłanie metod, tak jak jest to powszechnie stosowane w „statycznych” językach OOP, znajduje się gdzieś pomiędzy tymi dwoma skrajnościami: Klasa z góry deklaruje rodzaj wszystkich obsługiwanych operacji, więc są one statycznie znane i można je sprawdzić. Następnie możemy zbudować prostą tablicę przeglądową (VTable), która wskazuje na faktyczną implementację. Każdy obiekt zawiera wskaźnik do vtable. System typów gwarantuje, że każdy otrzymany obiekt będzie miał odpowiednią tabelę vtable, ale nie mamy pojęcia w czasie kompilacji, jaka jest wartość tej tabeli odnośników. Dlatego obiektów można używać do przekazywania funkcji jako danych (połowa powodów, dla których OOP i programowanie funkcji są równoważne). Tabele Vtab można łatwo zaimplementować w dowolnym języku obsługującym wskaźniki funkcji, takim jak C.

#define METHOD_CALL(object_ptr, name, ...) \
  (object_ptr)->vtable->name((object_ptr), __VA_ARGS__)

typedef struct {
    void (*sayHello)(const MyObject* this, const char* yourname);
} MyObject_VTable;

typedef struct {
    const MyObject_VTable* vtable;
    const char* name;
} MyObject;

static void MyObject_sayHello_normal(const MyObject* this, const char* yourname) {
  printf("Hello %s, I'm %s!\n", yourname, this->name);
}

static void MyObject_sayHello_alien(const MyObject* this, const char* yourname) {
  printf("Greetings, %s, we are the %s!\n", yourname, this->name);
}

static MyObject_VTable MyObject_VTable_normal = {
  .sayHello = MyObject_sayHello_normal,
};
static MyObject_VTable MyObject_VTable_alien = {
  .sayHello = MyObject_sayHello_alien,
};

static void sayHelloToMeredith(const MyObject* greeter) {
   // we have no idea what the VTable contents of my object are.
   // However, we do know it has a sayHello method.
   // This is dynamic dispatch right here!
   METHOD_CALL(greeter, sayHello, "Meredith");
}

int main() {
  // two objects with different vtables
  MyObject frank = { .vtable = &MyObject_VTable_normal, .name = "Frank" };
  MyObject zorg  = { .vtable = &MyObject_VTable_alien, .name = "Zorg" };

  sayHelloToMeredith(&frank); // prints "Hello Meredith, I'm Frank!"
  sayHelloToMeredith(&zorg); // prints "Greetings, Meredith, we are the Zorg!"
}

Ten rodzaj wyszukiwania metody jest również znany jako „dynamiczna wysyłka” i gdzieś pomiędzy wczesnym wiązaniem a późnym wiązaniem. Uważam dynamiczne wysyłanie metod za centralną właściwość definiującą programowanie OOP, a wszystko inne (np. Enkapsulacja, subtyping,…) jest drugorzędne. Pozwala nam wprowadzić polimorfizm do naszego kodu, a nawet dodać nowe zachowanie do fragmentu kodu bez konieczności jego ponownej kompilacji! W przykładzie C każdy może dodać nowy vtable i przekazać obiekt z tym vtable do sayHelloToMeredith().

Chociaż jest to późne wiązanie, nie jest to „ekstremalne późne wiązanie”, za którym opowiada się Kay. Zamiast modelu koncepcyjnego „wysyłanie metod za pomocą wskaźników funkcji” stosuje „wysyłanie metod poprzez przekazywanie komunikatów”. Jest to ważne rozróżnienie, ponieważ przekazywanie wiadomości jest znacznie bardziej ogólne. W tym modelu każdy obiekt ma skrzynkę odbiorczą, w której inne obiekty mogą umieszczać wiadomości. Obiekt odbierający może następnie spróbować zinterpretować tę wiadomość. Najbardziej znanym systemem OOP jest WWW. Tutaj wiadomości są żądaniami HTTP, a serwery są obiektami.

Na przykład mogę zapytać serwer programmers.stackexchange.se GET /questions/301919/. Porównaj to z notacją programmers.get("/questions/301919/"). Serwer może odrzucić tę prośbę lub odesłać mi błąd lub może dostarczyć mi twoje pytanie.

Moc przekazywania wiadomości polega na tym, że skaluje się bardzo dobrze: żadne dane nie są udostępniane (tylko przesyłane), wszystko może się odbywać asynchronicznie, a obiekty mogą interpretować wiadomości w dowolny sposób. To sprawia, że ​​przekazujący komunikat system OOP jest łatwo rozszerzalny. Mogę wysyłać wiadomości, które nie wszyscy rozumieją, albo odzyskać oczekiwany wynik lub błąd. Obiekt nie musi deklarować z góry, na które komunikaty odpowie.

To nakłada odpowiedzialność za utrzymanie poprawności na odbiorcę wiadomości, myśl zwaną także enkapsulacją. Np. Nie mogę odczytać pliku z serwera HTTP bez pytania go za pomocą wiadomości HTTP. Dzięki temu serwer HTTP może odrzucić moje żądanie, np. Jeśli nie mam uprawnień. W mniejszej skali OOP oznacza to, że nie mam dostępu do odczytu i zapisu do wewnętrznego stanu obiektu, ale muszę przejść przez metody publiczne. Serwer HTTP też nie musi mi podawać pliku. Może to być dynamicznie generowana zawartość z bazy danych. W rzeczywistym OOP mechanizm reakcji obiektu na komunikaty może zostać wyłączony bez zauważenia przez użytkownika. Jest to silniejsze niż „odbicie”, ale zwykle pełny protokół meta-obiektów. Mój powyższy przykład C nie może zmienić mechanizmu wysyłania w czasie wykonywania.

Możliwość zmiany mechanizmu wysyłania oznacza późne wiązanie, ponieważ wszystkie wiadomości są kierowane za pomocą kodu definiowanego przez użytkownika. Jest to niezwykle potężne: biorąc pod uwagę protokół metaobiektu, mogę dodawać funkcje, takie jak klasy, prototypy, dziedziczenie, klasy abstrakcyjne, interfejsy, cechy, wielokrotne dziedziczenie, wielozadaniowość, programowanie aspektowe, odbicie, zdalne wywoływanie metod, obiekty proxy itp. dla języka, który nie zaczyna się od tych funkcji. Ta moc ewolucji jest całkowicie nieobecna w bardziej statycznych językach, takich jak C #, Java lub C ++.

amon
źródło
4

Późne wiązanie odnosi się do tego, jak obiekty komunikują się ze sobą. Ideałem, który Alan stara się osiągnąć, jest możliwie luźne sprzężenie przedmiotów. Innymi słowy, że obiekt musi znać minimum, aby komunikować się z innym obiektem.

Dlaczego? Ponieważ zachęca to do samodzielnej zmiany części systemu i umożliwia mu wzrost i zmianę organiczną.

Na przykład w języku C # możesz napisać w metodzie dla obj1czegoś takiego obj2.doSomething(). Możesz traktować to jako obj1komunikację z obj2. Aby tak się stało w języku C #, obj1trzeba się trochę dowiedzieć obj2. Będzie musiał znać swoją klasę. Sprawdziłby, czy klasa ma wywoływaną metodę doSomethingi czy istnieje wersja tej metody, która przyjmuje zerowe parametry.

Teraz wyobraź sobie system, w którym wysyłasz wiadomość przez sieć lub podobny. możesz napisać coś takiego Runtime.sendMsg(ipAddress, "doSomething"). W takim przypadku nie musisz dużo wiedzieć o maszynie, z którą się komunikujesz; prawdopodobnie można się z nim skontaktować przez IP i zrobi coś, gdy otrzyma ciąg „doSomething”. Ale poza tym wiesz bardzo mało.

Teraz wyobraź sobie, że tak komunikują się przedmioty. Znasz adres i możesz wysyłać dowolne wiadomości na ten adres za pomocą funkcji „skrzynki pocztowej”. W tym przypadku obj1nie trzeba wiele wiedzieć obj2, wystarczy adres. Nie musi nawet wiedzieć, że to rozumie doSomething.

To właściwie sedno późnego wiązania. Teraz w językach, które go używają, takich jak Smalltalk i Objective-C, zwykle jest trochę cukru syntaktycznego, aby ukryć funkcję skrzynki pocztowej. Ale poza tym pomysł jest taki sam.

W języku C # można go replikować, w pewnym sensie, mając Runtimeklasę, która akceptuje ref obiektu i ciąg znaków i używa refleksji do znalezienia metody i wywołania jej (zacznie się komplikować z argumentami i zwracanymi wartościami, ale byłoby to możliwe brzydki).

Edycja: aby uniknąć zamieszania w odniesieniu do znaczenia późnego wiązania. W tej odpowiedzi mam na myśli późne wiązanie, ponieważ rozumiem, że Alan Kay miał na myśli to i zaimplementował je w Smalltalk. Nie jest to bardziej powszechne, nowoczesne użycie terminu, który ogólnie odnosi się do wysyłki dynamicznej. Ten ostatni obejmuje opóźnienie w rozwiązaniu dokładnej metody do czasu uruchomienia, ale nadal wymaga pewnych informacji o typie dla odbiornika w czasie kompilacji.

Alex
źródło