Объектно-ориентированное позднее связывание

11

В определении объекта Alan Kays есть определение, которое я частично не понимаю:

Для меня ООП означает только обмен сообщениями, локальное хранение и защиту, а также скрытие процесса состояния и крайнюю LateBinding всех вещей.

Но что означает «LateBinding»? Как я могу применить это на языке, как C #? И почему это так важно?

Лука Зулиан
источник
2
ООП в C #, вероятно, не тот тип ООП, который имел в виду Алан Кей.
Док Браун
Я согласен с вами, абсолютно ... примеры приветствуются на любых языках
Лука Зулиан

Ответы:

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

Это пример позднего связывания. Хотя строгая проверка типов необоснованна (проверка типов может выполняться только во время выполнения), она гораздо более гибкая и позволяет выражать понятия, которые не могут быть выражены в рамках статической типизации или раннего связывания. Например, мы можем добавлять новые функции во время выполнения.

Диспетчеризация методов, как это обычно реализуется в «статических» языках ООП, находится где-то посередине между этими двумя крайностями: класс заранее объявляет тип всех поддерживаемых операций, поэтому они статически известны и могут быть проверены на наличие типов. Затем мы можем построить простую таблицу поиска (VTable), которая указывает на фактическую реализацию. Каждый объект содержит указатель на vtable. Система типов гарантирует, что любой объект, который мы получим, будет иметь подходящую vtable, но мы не знаем во время компиляции, каково значение этой таблицы поиска. Следовательно, объекты могут использоваться для передачи функций в виде данных (половина причины, по которой ООП и программирование функций эквивалентны). 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!"
}

Этот вид поиска метода также известен как «динамическая диспетчеризация» и находится где-то между ранним и поздним связыванием. Я считаю, что динамическая диспетчеризация методов является центральным определяющим свойством программирования ООП, а все остальное (например, инкапсуляция, подтипы и т. Д.) Является вторичным. Это позволяет нам вводить полиморфизм в наш код и даже добавлять новое поведение к коду без необходимости его перекомпиляции! В примере C любой может добавить новый vtable и передать объект с этим vtable sayHelloToMeredith().

Несмотря на то, что это поздняя привязка, это не «крайняя поздняя привязка», которую предпочитает Кей. Вместо концептуальной модели «диспетчеризация метода через указатели на функции» он использует «диспетчеризацию метода через передачу сообщения». Это важное различие, потому что передача сообщений гораздо более общая. В этой модели каждый объект имеет папку входящих сообщений, в которую другие объекты могут помещать сообщения. Получающий объект может затем попытаться интерпретировать это сообщение. Наиболее известной системой ООП является WWW. Здесь сообщения - это HTTP-запросы, а серверы - объекты.

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

Сила передачи сообщений заключается в том, что она очень хорошо масштабируется: никакие данные не передаются (только передаются), все может происходить асинхронно, а объекты могут интерпретировать сообщения так, как им нравится. Это делает систему передачи сообщений ООП легко расширяемой. Я могу отправлять сообщения, которые не все могут понять, и либо получить ожидаемый результат, либо ошибку. Объект не должен заранее объявлять, на какие сообщения он будет отвечать.

Это возлагает ответственность за поддержание правильности на получателя сообщения, мысль, также известная как инкапсуляция. Например, я не могу прочитать файл с HTTP-сервера, не запросив его через HTTP-сообщение. Это позволяет HTTP-серверу отклонить мой запрос, например, если у меня нет разрешений. В меньшем масштабе ООП это означает, что у меня нет доступа на чтение и запись к внутреннему состоянию объекта, но я должен пройти через открытые методы. HTTP-сервер тоже не должен обслуживать мне файл. Это может быть динамически генерируемый контент из БД. В реальном ООП механизм реагирования объекта на сообщения может быть отключен без уведомления пользователя. Это сильнее, чем «отражение», но обычно это полный протокол мета-объекта. Мой пример 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. Это не более распространенное, современное использование термина, который обычно относится к динамической отправке. Последний покрывает задержку в разрешении точного метода до времени выполнения, но все же требует некоторой информации о типе для получателя во время компиляции.

Alex
источник