"Vinculação" refere-se ao ato de resolver um nome de método para um pedaço de código invocável. Geralmente, a chamada de função pode ser resolvida no tempo de compilação ou no tempo do link. Um exemplo de linguagem que usa ligação estática é C:
int foo(int x);
int main(int, char**) {
printf("%d\n", foo(40));
return 0;
}
int foo(int x) { return x + 2; }
Aqui, a chamada foo(40)
pode ser resolvida pelo compilador. Este início permite certas otimizações, como inlining. As vantagens mais importantes são:
- nós podemos fazer a verificação de tipo
- nós podemos fazer otimizações
Por outro lado, alguns idiomas adiam a resolução da função até o último momento possível. Um exemplo é o Python, onde podemos redefinir símbolos rapidamente:
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 é um exemplo de ligação tardia. Embora faça uma verificação rigorosa de tipo de maneira não razoável (a verificação de tipo só pode ser feita em tempo de execução), é muito mais flexível e nos permite expressar conceitos que não podem ser expressos dentro dos limites da digitação estática ou da ligação antecipada. Por exemplo, podemos adicionar novas funções em tempo de execução.
O despacho de método, como geralmente implementado nas linguagens OOP "estáticas", está entre esses dois extremos: Uma classe declara o tipo de todas as operações suportadas desde o início, portanto, elas são conhecidas estaticamente e podem ser checadas. Podemos então criar uma tabela de pesquisa simples (VTable) que aponte para a implementação real. Cada objeto contém um ponteiro para uma tabela. O sistema de tipos garante que qualquer objeto obtido tenha uma vtable adequada, mas não temos idéia em tempo de compilação qual é o valor dessa tabela de pesquisa. Portanto, os objetos podem ser usados para transmitir funções como dados (metade da razão pela qual a OOP e a programação de funções são equivalentes). Vtables pode ser facilmente implementado em qualquer idioma que suporte ponteiros de função, 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!"
}
Esse tipo de pesquisa de método também é conhecido como "despacho dinâmico" e em algum momento entre a ligação inicial e a ligação tardia. Considero que o envio de método dinâmico é a propriedade definidora central da programação OOP, com qualquer outra coisa (por exemplo, encapsulamento, subtipagem ...) como secundária. Ele nos permite introduzir polimorfismo em nosso código e até adicionar novo comportamento a um pedaço de código sem precisar recompilar! No exemplo C, qualquer pessoa pode adicionar uma nova vtable e passar um objeto com essa vtable para sayHelloToMeredith()
.
Embora seja uma ligação tardia, essa não é a "ligação tardia extrema" preferida por Kay. Em vez do modelo conceitual “despacho de método por meio de ponteiros de função”, ele usa “despacho de método por passagem de mensagem”. Essa é uma distinção importante porque a passagem de mensagens é muito mais geral. Nesse modelo, cada objeto tem uma caixa de entrada onde outros objetos podem colocar mensagens. O objeto receptor pode então tentar interpretar essa mensagem. O sistema OOP mais conhecido é a WWW. Aqui, as mensagens são solicitações HTTP e servidores são objetos.
Por exemplo, posso perguntar ao servidor programmers.stackexchange.se GET /questions/301919/
. Compare isso com a notação programmers.get("/questions/301919/")
. O servidor pode recusar esta solicitação ou me devolver um erro ou pode me responder sua pergunta.
O poder da passagem de mensagens é que ele se adapta muito bem: nenhum dado é compartilhado (apenas transferido), tudo pode acontecer de forma assíncrona e os objetos podem interpretar as mensagens da maneira que desejarem. Isso torna uma mensagem que passa pelo sistema OOP facilmente extensível. Posso enviar mensagens que nem todos podem entender e recuperar meu resultado esperado ou um erro. O objeto não precisa declarar antecipadamente a quais mensagens ele responderá.
Isso coloca a responsabilidade de manter a correção no receptor de uma mensagem, um pensamento também conhecido como encapsulamento. Por exemplo, não consigo ler um arquivo de um servidor HTTP sem solicitá-lo por meio de uma mensagem HTTP. Isso permite que o servidor HTTP recuse minha solicitação, por exemplo, se eu não tiver permissões. Em OOP de menor escala, isso significa que não tenho acesso de leitura e gravação ao estado interno de um objeto, mas devo passar por métodos públicos. Um servidor HTTP também não precisa me servir um arquivo. Pode ser conteúdo gerado dinamicamente a partir de um banco de dados. Na OOP real, o mecanismo de como um objeto responde às mensagens pode ser desativado, sem que o usuário perceba. Isso é mais forte que a "reflexão", mas geralmente um protocolo completo de meta-objeto. Meu exemplo de C acima não pode alterar o mecanismo de despacho em tempo de execução.
A capacidade de alterar o mecanismo de despacho implica em ligação tardia, pois todas as mensagens são roteadas por código definido pelo usuário. E isso é extremamente poderoso: dado um protocolo de meta-objeto, posso adicionar recursos como classes, protótipos, herança, classes abstratas, interfaces, características, herança múltipla, despacho múltiplo, programação orientada a aspectos, reflexão, invocação de método remoto, objetos proxy etc. para um idioma que não inicia com esses recursos. Esse poder de evoluir está completamente ausente de linguagens mais estáticas, como C #, Java ou C ++.