程式語言中的介面,在個體之間協議互動行為的多種形式
racklin 說: 我的重點還是只放在 "關注類別是否有實作方法" 也就是 "介面" 的這個概念, 因為原文是討論這個議題
. 嗯,我大概是跳太快了。我清楚 interface 是什麼。所以我的回應是在說明「個體之間如何協議互動行為」亦即「軟體合約」的形式。
以C/C++為例,在早期,程序員學了 C++ 可是還是要寫 C 程式的時代,我們會自己用 C 語言實作類別繼承、動態連結等觀念。但我們用的是 C compiler 而非 C++ compiler ,所以很多事我們必須自己處理。其中一點就是個體行為的協議。方法一、在函數文件上說明傳入的個體需擁有哪些行為,我在函數中會檢查此個體是否擁有此行為(函數指標是否為給定了)。方法二、限定一個 struct (只有純函數指標宣告),呼叫者要自己填一張函數指標表傳入,這其實就是 interface 的概念。

例如下列的程式碼,就是用 struct 宣告一張個體行為介面表。完整程式碼可見於 bbslib-20010331。
/*
string_i.h Header file of string interface.
Copyright (C) 1998, Sh Yunchen, rock@bbs.isu.edu.tw
Licensed by GNU Lesser General Public License.
*/
#if !defined( __STRING_I_H )
#define __STRING_I_H
struct stringinterface
{
void* striobj;
size_t (*length)(const void*);
char* (*text)(const void*);
char* (*strcpy)(void*, const char*);
};
typedef struct stringinterface string_i;
#endif // defined( __STRING_I_H )
使用方式通常如下列所示。宣告一個 buffer struct ,定義需要的函數。建構函數中要配置並指派函數指標。注意,這是 C ,不是 C++ 。一切要自己來。用 Java 的用語說明,此 buffer 類別的用途,在於將 primitive type 的字串轉成 object 。亦即 buffer 是一個 Wrapper Class 。最後在 main 和 print_content_and_length 中展示了近似 C++ 的敘述。
/*
buffer.c String buffer routines.
Copyright (C) 1999, Sh Yunchen, shirock@mail.educities.edu.tw
Licensed by GNU Lesser General Public License.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "string_i.h"
struct buffer
{
string_i striobj;
char* bufptr;
void* freeptr;
};
typedef struct buffer buffer_t;
size_t buffer_length(const buffer_t*b) {
return strlen(b->bufptr);
}
char* buffer_text(const buffer_t*b) {
return b->bufptr;
}
char* buffer_strcpy(buffer_t*b, const char*src) {
return strcpy(b->bufptr, src);
}
buffer_t* buffer_construct(buffer_t*b, char*bufptr) {
buffer_t *tmp;
if( b==NULL )
tmp=(buffer_t *) malloc(sizeof(buffer_t));
else
tmp=b;
memset(tmp,0,sizeof(buffer_t));
// all of the elements set to clear (null).
tmp->striobj.striobj = tmp;
tmp->striobj.length = (size_t (*)(const void*)) buffer_length;
tmp->striobj.text = (char* (*)(const void*)) buffer_text;
tmp->striobj.strcpy = (char* (*)(void*, const char*)) buffer_strcpy;
bufptr[0] = '\0';
tmp->bufptr = bufptr;
if( b==NULL )
tmp->freeptr = tmp; //MALLOC
else
tmp->freeptr = NULL;
return tmp;
}
/*
Define a function use string_i.
*/
void print_content_and_length(string_i* str) {
printf("Length of %s is %d\n",
str->text(str), str->length(str));
}
int main() {
buffer_t buf;
char s[1024];
buffer_construct(&buf, s);
buf.striobj.strcpy(&buf, "hello world");
print_content_and_length((string_i*) &buf);
//因為address對齊了, &buf = &buf->striobj.
return 0;
}
在當時,因為我們總是用強迫轉型的方式傳遞個體,所以 struct 中的宣告,僅是告知程序員應有什麼,對 compiler 是沒用的。程序員要自己掌握型態,軟體合約在程序員腦中、口中與文件中。
當然,C++/Java compiler 就不用那麼麻煩了。它會幫我們處理這些小事。它們會幫我們維護那張表,而 interface 的宣告內容就成為函數的說明文件之一。若以虛擬碼巨集表示:
declare interface ObjectAccess
method get(k)
method set(k, v)
end
define class Obj implements ObjectAccess
id
end
#expand Obj
type Obj
id
method get(k)
method set(k, v)
end
#end
function test(interface Abc o);
var a;
test(a);
#expand
#if a has method 'get' and method 'set'
call test(a);
#else
#throw TYPE ERROR
#end
#end
上述動作隱含於 C++/Java 的 compile 動作中,而在動態語言中往往是明顯地呈現在程式碼中。這並無不妥,因為動態語言往往具有中介語言/巨集語言的泛型表達能力。
function test(o) {
if o.has_method?('get') and o.has_method?('set')
do something
else
throw TYPE ERROR
end
}
對於錯誤捕捉之事。靜態語言仰賴各種宣告資訊,提供 compiler 在編譯時期核對。但我因為以往有 C 語言實作的經驗,所以向來不太倚重 interface 防錯能力。
另一方面,動態語言沒有編譯時期,它們在執行時期才能確定個體資訊,故必須仰賴更動態的處理策略。現行最有效的策略就是玩真的,提供 test case 供 tester 在執行時期測試。
嗯。我這幾天就一直在想,為什麼我在寫動態語言程式時,不像寫 C/C++ 那麼強調型態宣告,卻不覺得程式會出錯?後來想到了,那是因為有 TDD。而且 tester 所產生的結果更可靠。