Enlace tardío orientado a objetos

11

En la definición de Alan Kays de orientado a objetos existe esta definición que parcialmente no entiendo:

OOP para mí significa solo mensajes, retención local y protección y ocultación de procesos estatales, y LateBinding extremo de todas las cosas.

Pero, ¿qué significa "LateBinding"? ¿Cómo puedo aplicar esto en un lenguaje como C #? ¿Y por qué es esto tan importante?

Luca Zulian
fuente
2
OOP en C # probablemente no sea el tipo de OOP que Alan Kay tenía en mente.
Doc Brown
Estoy de acuerdo con usted, absolutamente ... ejemplos son bienvenidos en cualquier idioma
Luca Zulian

Respuestas:

14

"Enlace" se refiere al acto de resolver el nombre de un método a un fragmento de código invocable. Por lo general, la llamada a la función se puede resolver en tiempo de compilación o en tiempo de enlace. Un ejemplo de un lenguaje que usa enlace estático es C:

int foo(int x);

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

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

Aquí, la llamada foo(40)puede ser resuelta por el compilador. Esto temprano permite ciertas optimizaciones como la alineación. Las ventajas más importantes son:

  • podemos hacer verificaciones de tipo
  • podemos hacer optimizaciones

Por otro lado, algunos idiomas difieren la resolución de la función hasta el último momento posible. Un ejemplo es Python, donde podemos redefinir símbolos sobre la marcha:

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

Este es un ejemplo de encuadernación tardía. Si bien hace que la verificación de tipos rigurosa sea irrazonable (la verificación de tipos solo se puede hacer en tiempo de ejecución), es mucho más flexible y nos permite expresar conceptos que no se pueden expresar dentro de los límites de la escritura estática o la vinculación temprana. Por ejemplo, podemos agregar nuevas funciones en tiempo de ejecución.

El envío de métodos implementado comúnmente en lenguajes OOP "estáticos" se encuentra en algún punto entre estos dos extremos: una clase declara el tipo de todas las operaciones admitidas por adelantado, por lo que estas son estáticamente conocidas y se pueden verificar. Luego podemos construir una tabla de búsqueda simple (VTable) que apunte a la implementación real. Cada objeto contiene un puntero a una vtable. El sistema de tipos garantiza que cualquier objeto que obtengamos tenga una vtable adecuada, pero en el momento de la compilación no tenemos idea de cuál es el valor de esta tabla de búsqueda. Por lo tanto, los objetos se pueden usar para pasar funciones como datos (la mitad de la razón por la cual la programación de funciones y OOP son equivalentes). Vtables se puede implementar fácilmente en cualquier lenguaje que admita punteros de función, como 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!"
}

Este tipo de búsqueda de método también se conoce como "despacho dinámico", y en algún punto intermedio entre el enlace temprano y el enlace tardío. Considero que el despacho de métodos dinámicos es la propiedad de definición central de la programación OOP, con cualquier otra cosa (por ejemplo, encapsulación, subtipo, ...) como secundaria. ¡Nos permite introducir polimorfismo en nuestro código e incluso agregar un nuevo comportamiento a un fragmento de código sin tener que volver a compilarlo! En el ejemplo de C, cualquiera puede agregar una nueva vtable y pasarle un objeto con esa vtable sayHelloToMeredith().

Si bien este es un enlace tardío, este no es el "enlace tardío extremo" favorecido por Kay. En lugar del modelo conceptual "envío de métodos a través de punteros de función", utiliza "envío de métodos a través del paso de mensajes". Esta es una distinción importante porque el paso de mensajes es mucho más general. En este modelo, cada objeto tiene una bandeja de entrada donde otros objetos pueden poner mensajes. El objeto receptor puede intentar interpretar ese mensaje. El sistema OOP más conocido es el WWW. Aquí, los mensajes son solicitudes HTTP y los servidores son objetos.

Por ejemplo, puedo preguntarle al servidor programmers.stackexchange.se GET /questions/301919/. Compare esto con la notación programmers.get("/questions/301919/"). El servidor puede rechazar esta solicitud o devolverme un error, o puede responderme su pregunta.

El poder de pasar mensajes es que se escala muy bien: no se comparten datos (solo se transfieren), todo puede suceder de forma asincrónica y los objetos pueden interpretar los mensajes como quieran. Esto hace que un mensaje que pasa el sistema OOP sea fácilmente extensible. Puedo enviar mensajes que no todos pueden entender y recuperar mi resultado esperado o un error. El objeto no necesita declarar por adelantado a qué mensajes responderá.

Esto pone la responsabilidad de mantener la corrección en el receptor de un mensaje, un pensamiento también conocido como encapsulación. Por ejemplo, no puedo leer un archivo de un servidor HTTP sin pedirlo a través de un mensaje HTTP. Esto permite que el servidor HTTP rechace mi solicitud, por ejemplo, si no tengo permisos. En OOP de menor escala, esto significa que no tengo acceso de lectura y escritura al estado interno de un objeto, sino que debo pasar por métodos públicos. Un servidor HTTP tampoco tiene que servirme un archivo. Podría ser contenido generado dinámicamente desde una base de datos. En OOP real, el mecanismo de cómo un objeto responde a los mensajes se puede cambiar sin que el usuario lo note. Esto es más fuerte que la "reflexión", pero generalmente es un protocolo completo de metaobjetos. Mi ejemplo C anterior no puede cambiar el mecanismo de envío en tiempo de ejecución.

La capacidad de cambiar el mecanismo de envío implica un enlace tardío, ya que todos los mensajes se enrutan a través de un código definible por el usuario. Y esto es extremadamente poderoso: dado un protocolo de metaobjetos, puedo agregar características como clases, prototipos, herencia, clases abstractas, interfaces, rasgos, herencia múltiple, despacho múltiple, programación orientada a aspectos, reflexión, invocación de método remoto, objetos proxy, etc. a un lenguaje que no comienza con estas características. Este poder para evolucionar está completamente ausente de lenguajes más estáticos como C #, Java o C ++.

amon
fuente
4

El enlace tardío se refiere a cómo los objetos se comunican entre sí. El ideal que Alan está tratando de lograr es que los objetos estén tan flojos como sea posible. En otras palabras, un objeto necesita saber el mínimo posible para comunicarse con otro objeto.

¿Por qué? Porque eso fomenta la capacidad de cambiar partes del sistema de forma independiente y le permite crecer y cambiar orgánicamente.

Por ejemplo, en C # puede escribir un método para obj1algo así obj2.doSomething(). Puedes ver esto como obj1comunicarte con obj2. Para que esto suceda en C #, obj1necesita saber un poco sobre obj2. Habría necesitado saber su clase. Habría comprobado que la clase tiene un método llamado doSomethingy que hay una versión de ese método que toma cero parámetros.

Ahora imagine un sistema donde envía un mensaje a través de una red o similar. se podría escribir algo así como Runtime.sendMsg(ipAddress, "doSomething"). En este caso, no necesita saber mucho sobre la máquina con la que se está comunicando; presumiblemente se puede contactar a través de IP y hará algo cuando reciba la cadena "doSomething". Pero por lo demás sabes muy poco.

Ahora imagine que así es como se comunican los objetos. Usted conoce una dirección y puede enviar mensajes arbitrarios a esa dirección con algún tipo de función de "buzón de correo". En este caso, obj1no necesita saber mucho obj2, solo su dirección. Ni siquiera necesita saber que entiende doSomething.

Eso es más o menos el quid de la unión tardía. Ahora, en los lenguajes que lo usan, como Smalltalk y ObjectiveC, generalmente hay un poco de azúcar sintáctica para ocultar la función de buzón. Pero por lo demás, la idea es la misma.

En C #, podría replicarlo, más o menos, al tener una Runtimeclase que acepte una referencia de objeto y una cadena y use la reflexión para encontrar el método e invocarlo (comenzará a complicarse con argumentos y valores de retorno, pero sería posible aunque feo).

Editar: para disipar cierta confusión con respecto al significado del enlace tardío. En esta respuesta me refiero al enlace tardío porque entiendo que Alan Kay lo quiso decir y lo implementó en Smalltalk. No es el uso más común y moderno del término lo que generalmente se refiere al despacho dinámico. Este último cubre el retraso en la resolución del método exacto hasta el tiempo de ejecución, pero aún requiere cierta información de tipo para el receptor en tiempo de compilación.

Alex
fuente