Пізнє зв’язування, орієнтоване на об'єкти


11

У визначенні Alan Kays Орієнтоване на об'єкт є таке визначення, яке частково я не розумію:

OOP для мене означає лише обмін повідомленнями, локальне збереження та захист та приховування державно-процесу та екстремальне пізнє зв’язування всіх речей.

Але що означає "пізній зв'язок"? Як я можу застосувати це на такій мові, як C #? І чому це так важливо?



2
OOP в C # - це, мабуть, не такий тип ООП, який мав на увазі Алан Кей.
Doc Brown

Я згоден з вами, абсолютно ... приклади вітаються на будь-яких мовах
Лука Зуліан

Відповіді:


14

"Прив'язка" відноситься до акта розв'язання назви методу до фрагменту викликаного коду. Зазвичай виклик функції може бути вирішений під час компіляції або під час зв'язку. Прикладом мови, що використовує статичне зв'язування, є C:

int foo(int x);

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

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

Тут виклик foo(40)може бути вирішений компілятором. Це рано дозволяє певні оптимізації, такі як вбудована лінія. Найважливіші переваги:

  • ми можемо зробити перевірку типу
  • ми можемо робити оптимізацію

З іншого боку, деякі мови відкладають роздільну здатність функції на останній можливий момент. Приклад - Python, де ми можемо переглядати символи на льоту:

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

Це приклад пізнього зв’язування. Хоча це робить необґрунтовану перевірку типу необґрунтованою (перевірку типу можна проводити лише під час виконання), вона набагато гнучкіша і дозволяє нам висловлювати поняття, які не можуть бути виражені в межах статичного набору тексту або раннього прив’язки. Наприклад, ми можемо додавати нові функції під час виконання.

Диспетчер методів, як зазвичай реалізований у "статичних" мовах OOP, знаходиться десь між цими двома крайнощами: Клас оголошує тип усіх підтримуваних операцій на передній панелі, тому вони є статично відомими і можуть бути перевірені. Потім ми можемо побудувати просту таблицю пошуку (VTable), яка вказує на фактичну реалізацію. Кожен об'єкт містить вказівник на vtable. Система типу гарантує, що будь-який об’єкт, який ми отримаємо, матиме відповідний vtable, але ми не маємо уявлення під час компіляції, яке значення має ця таблиця пошуку. Тому об'єкти можна використовувати для передачі функцій навколо як даних (половина причини, через яку OOP та функціонування функцій є рівнозначними). Vtables можна легко реалізувати будь-якою мовою, яка підтримує функціональні покажчики, наприклад 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!"
}

Цей вид пошуку методів також відомий як "динамічна відправка", і десь між ранньою і пізньою зв'язуваннями. Я вважаю диспетчеризацію динамічних методів центральною визначальною властивістю програмування OOP, причому будь-що інше (наприклад, інкапсуляція, підтипізація ...) є другорядним. Це дозволяє нам ввести поліморфізм у наш код і навіть додати нову поведінку до фрагмента коду без необхідності його перекомпілювати! У прикладі C кожен може додати новий vtable і передати об’єкт із цим vtable sayHelloToMeredith().

Незважаючи на те, що це зав'язування пізно, це не є "крайнім пізнім зв'язуванням", яке сприяє Кей. Замість концептуальної моделі "метод відправки через функціональні покажчики" він використовує "метод відправки через передачу повідомлення". Це важлива відмінність, оскільки передача повідомлень набагато загальніша. У цій моделі кожен об’єкт має папку "Вхідні", куди інші об'єкти можуть розміщувати повідомлення. Потім об'єкт, що отримує, може спробувати інтерпретувати це повідомлення. Найвідомішою системою ООП є WWW. Тут повідомлення - це HTTP-запити, а сервери - об’єкти.

Наприклад, я можу запитати сервер programmers.stackexchange.se GET /questions/301919/. Порівняйте це з позначенням programmers.get("/questions/301919/"). Сервер може відмовити в цьому запиті або надіслати мені помилку, або він може надіслати мені ваше запитання.

Сила передачі повідомлень полягає в тому, що вона масштабується дуже добре: ніякі дані не обмінюються (передаються тільки), все може відбуватися асинхронно, а об’єкти можуть інтерпретувати повідомлення, як вони завгодно. Це робить повідомлення, що передають систему OOP, легко розширюється. Я можу надсилати повідомлення, які не всі можуть зрозуміти, і або повернути очікуваний результат, або помилку. Об'єкт не повинен попередньо оголошувати, на які повідомлення він відповість.

Це покладає відповідальність за збереження правильності на одержувача повідомлення, думка, також відома як інкапсуляція. Наприклад, я не можу прочитати файл із сервера HTTP, не запитуючи його через повідомлення HTTP. Це дозволяє серверу HTTP відмовитись від мого запиту, наприклад, якщо у мене відсутні дозволи. У меншому масштабі OOP це означає, що я не маю доступу для читання-запису до внутрішнього стану об'єкта, але я повинен пройти публічні методи. HTTP-сервер також не повинен мені подавати файл. Це може бути динамічно генерований контент із БД. У реальному OOP механізм того, як об’єкт реагує на повідомлення, може бути вимкнений, не помічаючи користувача. Це сильніше, ніж "відображення", але, як правило, повний протокол мета-об'єктів. Мій приклад C вище не може змінити механізм відправки під час виконання.

Можливість зміни механізму відправки передбачає пізнє прив'язування, оскільки всі повідомлення перенаправляються через визначений користувачем код. І це надзвичайно потужно: даючи протокол мета-об’єкта, я можу додати такі функції, як класи, прототипи, успадкування, абстрактні класи, інтерфейси, риси, багатонаступне успадкування, мультидиспетчеризація, орієнтоване на аспекти програмування, рефлексія, виклик віддаленого методу, проксі-об'єкти тощо до мови, яка не починається з цих функцій. Ця здатність розвиватися повністю відсутня у більш статичних мовах, таких як C #, Java або C ++.


4

Пізнє зв’язування означає, як об'єкти спілкуються один з одним. Ідеал, який Алан намагається досягти, - це об'єкти як можна більш вільно з'єднані. Іншими словами, об’єкт повинен знати мінімально можливий, щоб спілкуватися з іншим об'єктом.

Чому? Тому що це заохочує здатність самостійно змінювати частини системи і дає їй можливість органічно рости та змінюватися.

Наприклад, у C # ви можете написати метод для obj1чогось подібного obj2.doSomething(). Ви можете поглянути на це як на obj1спілкування obj2. Щоб це відбулося в C #, obj1потрібно знати трохи obj2. Потрібно було б знати свій клас. Він би перевірив, що в класі є метод, який називається, doSomethingі що існує версія цього методу, яка приймає нульові параметри.

Тепер уявіть систему, де ви надсилаєте повідомлення через мережу чи подібні. ви можете написати щось подібне Runtime.sendMsg(ipAddress, "doSomething"). У цьому випадку вам не потрібно багато знати про машину, з якою ви спілкуєтесь; Імовірно, з ним можна зв’язатися через IP, і він щось зробить, коли отримає рядок "doSomething". Але інакше ви знаєте дуже мало.

А тепер уявіть, як спілкуються об’єкти. Ви знаєте адресу, і ви можете надсилати до неї довільні повідомлення за допомогою якоїсь функції "поштової скриньки". У цьому випадку obj1не потрібно дуже багато знати про це obj2, лише це адреса. Навіть не потрібно знати, що він розуміє doSomething.

Це в значній мірі суть пізнього зв'язування. Зараз у таких мовах, як Smalltalk та ObjectiveC, зазвичай є трохи синтаксичного цукру, щоб приховати функцію поштової скриньки. Але в іншому випадку ідея та ж.

У C # ви можете його повторити, на зразок, мати Runtimeклас, який приймає об'єкт ref та рядок і використовує відображення для пошуку методу та виклику його (він почне ускладнюватися аргументами та повертатими значення, але це можливо, хоча потворний).

Редагувати: щоб зменшити деяку плутанину щодо значення пізнього зв’язування. У цій відповіді я маю на увазі пізнє прив'язування, наскільки я розумію, це мав на увазі Алан Кей і реалізував це в Smalltalk. Це не більш поширене сучасне використання терміна, який, як правило, відноситься до динамічної відправки. Останній покриває затримку у вирішенні точного методу до часу виконання, але все ж вимагає певної інформації для приймача під час компіляції.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.