最近更新: 2007-06-30

程式語言中的介面,在個體之間協議互動行為的多種形式

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 所產生的結果更可靠。

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/3556355.html