Ligação tardia orientada a objeto

11

Na Definição de Orientação a Objetos de Alan Kays, existe esta definição que parcialmente não entendo:

OOP para mim significa apenas mensagens, retenção e proteção local e ocultação de processos estatais e LateBinding extremo de todas as coisas.

Mas o que significa "LateBinding"? Como posso aplicar isso em um idioma como C #? E por que isso é tão importante?

Luca Zulian
fonte
2
POO em C # provavelmente não é o tipo de POO que Alan Kay tinha em mente.
Doc Brown
Concordo com você, absolutamente ... exemplos são bem-vindos em qualquer idioma
Luca Zulian

Respostas:

14

"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 ++.

amon
fonte
4

A ligação tardia se refere a como os objetos se comunicam. O ideal que Alan está tentando alcançar é que os objetos sejam tão fracamente acoplados quanto possível. Em outras palavras, um objeto precisa saber o mínimo possível para se comunicar com outro objeto.

Por quê? Porque isso incentiva a capacidade de alterar partes do sistema de forma independente e permite que ele cresça e mude organicamente.

Por exemplo, em C #, você pode escrever um método para obj1algo como obj2.doSomething(). Você pode ver isso como se obj1comunicar obj2. Para que isso aconteça em C #, é obj1necessário conhecer bastante obj2. Será necessário conhecer sua classe. Teria verificado se a classe possui um método chamado doSomethinge se existe uma versão desse método que aceita zero parâmetros.

Agora imagine um sistema para o qual você está enviando uma mensagem através de uma rede ou similar. você pode escrever algo como Runtime.sendMsg(ipAddress, "doSomething"). Nesse caso, você não precisa saber muito sobre a máquina com a qual está se comunicando; presumivelmente, pode ser contatado via IP e fará alguma coisa quando receber a string "doSomething". Mas, caso contrário, você sabe muito pouco.

Agora imagine que é assim que os objetos se comunicam. Você conhece um endereço e pode enviar mensagens arbitrárias para esse endereço com algum tipo de função "caixa postal". Nesse caso, obj1não precisa saber muito obj2, apenas o endereço. Nem precisa saber que entende doSomething.

Esse é basicamente o ponto crucial da ligação tardia. Agora, em idiomas que o utilizam, como Smalltalk e ObjectiveC, geralmente há um pouco de açúcar sintático para ocultar a função de caixa postal. Mas, caso contrário, a ideia é a mesma.

Em C #, você pode replicá-lo, meio que, tendo uma Runtimeclasse que aceita um objeto ref e uma string e usa reflexão para encontrar o método e invocá-lo (ele começará a ficar complicado com argumentos e valores de retorno, mas seria possível feio).

Editar: para acalmar alguma confusão com relação ao significado da ligação tardia. Nesta resposta, estou me referindo à ligação tardia, pois entendo que Alan Kay quis dizer isso e o implementou no Smalltalk. Não é o uso moderno e mais comum do termo que geralmente se refere ao envio dinâmico. O último cobre o atraso na resolução do método exato até o tempo de execução, mas ainda requer algumas informações de tipo para o receptor em tempo de compilação.

Alex
fonte