2012年8月8日水曜日

インターフェース (interface)

プログラミング言語 Cでインターフェース機能を使うことは難しくはない。

以前の例題の Interfaceクラスを少し改造して UpDownインターフェースを実装する例を示す。


インターフェースのヘッダ (header of interface)

UpDownインターフェースのヘッダはつぎの通り。
/* 7.Aug.2012 kei Moses Kissinger */

#ifndef _Included_UpDown
#define _Included_UpDown

typedef struct UpDown* UpDown;

struct UpDown {
        void (*up)(void* this);
        void (*down)(void* this);
};

#endif /* _Included_UpDown */

このように、構造体のメンバに関数ポインタを使用するのが一般的。

UpDownインターフェースは Integerクラス以外からも使えるため、引数の thisの型は void*にしてある。

もし Integerクラスでしか使用しないなら、わざわざインターフェースにする意味はほとんどないだろう。

実装クラスのヘッダ (header of implementation class)

UpDownインターフェースを実装する Integerクラスのヘッダファイルはつぎの通り。
/* 4.7.Aug.2012 kei */

#ifndef _Included_Integer
#define _Included_Integer

typedef struct Integer* Integer;

#include <UpDown.h>

extern UpDown Integer_interface_UpDown;

extern Integer new_Integer(int value);

extern void Integer_finalize(Integer this);

extern int Integer_getValue(Integer this);

extern void Integer_up(Integer this);

extern void Integer_down(Integer this);

#endif /* _Included_Integer */

Integer_interface_UpDownがインターフェースの構造体へのポインタ。

Integer_upと Integer_down関数はインターフェースを介さずにも呼べるようにするためのもの。インターフェース経由でしか呼ばないなら不要である。

実装 (implementation)

UpDownインターフェースを実装した Integerクラスのソースファイルはつぎの通り。
/* 4.7.Aug.2012 kei */

#include 

struct Integer {
        int value;
};

#include <malloc.h>

Integer new_Integer(int value) {
        Integer this = (Integer)malloc(sizeof(struct Integer));

        if (this != NULL) {
                this->value = value;
        }

        return this;
}

void Integer_finalize(Integer this) {
        free(this);
}

int Integer_getValue(Integer this) {
        return this->value;
}

void Integer_up(Integer this) {
        ++this->value;
}

void Integer_down(Integer this) {
        --this->value;
}

static struct UpDown interface_UpDown = {
        (void (*)(void*))Integer_up,
        (void (*)(void*))Integer_down
};

UpDown Integer_interface_UpDown = &interface_UpDown;

Integer_up, Integer_downは通常の public関数同様に記述しているが、up, downをインターフェース経由でしか利用しないなら、staticな関数として非公開にし、名前も increment, decrementのようなインターフェースの関数名とは異なるものにしても良い。その場合、関数名は他のソースファイルとは衝突する恐れが無いため、命名規約に沿った長い名前を付ける必要もない。

struct UpDownにて、upと down関数のポインタを設定している。 このとき、インターフェースの関数の型に合わせる為、キャストが必要となる。 ちょっと残念。

参考のため、ビルドの例も付けておく。
$ make install
cc -g -I /export/home/kei/c/core/include -I /export/home/kei/c/experimental/include
 -I /export/home/kei/c/io/include -I /export/home/kei/c/lang/include -I /export/home/
kei/c/message/include -I /export/home/kei/c/unit/include -I /export/home/kei/c/util/
include -I /export/home/kei/c/x11/include -I ../../char/include -I ../../class1/
include -D_REENTRANT  -c -o Integer.o Integer.c
ar cvrf "libclass1.a" Integer.o ;
r - Integer.o
mkdir -p /export/home/kei/sub/class1/lib;
libclass1.a updated.
$ 

なお、この方式では UpDown.cファイルは存在しない。

使用例 (example)

CUnitを使って使用例を示す。
/* 4.7.Aug.2012 kei */

#define import_lwd_unit_Assert
#define import_lwd_unit_CUnit

#include <Integer.h>
#include <lwd/unit/Assert.h>
#include <lwd/unit/CUnit.h>

static Integer instance;

void setUp() {
        instance = new_Integer(777);
}

void tearDown() {
        Integer_finalize(instance);
}

void testNewAndFinalize() {
        /* setUp and tearDown are invoked. */
}

void testGetValue() {
        int value = Integer_getValue(instance);

        Assert_equalsInt("A value must be 777.", 777, value);
}

void testUp() {
        UpDown upDown = Integer_interface_UpDown;
        upDown->up(instance);

        int value = Integer_getValue(instance);
        Assert_equalsInt("A value must be 778.", 778, value);
}

void testDown() {
        UpDown upDown = Integer_interface_UpDown;
        upDown->down(instance);

        int value = Integer_getValue(instance);
        Assert_equalsInt("A value must be 776.", 776, value);
}

int main(int argc, char** argv) {
        return CUnit_test(argc, argv);
}
testUp, testDown関数で各々 UpDownインターフェースを求め、up, down関数を呼び出している。
Integer_interface_UpDownのままでも up, down関数は呼べちゃうが、そうすると抽象化の意味は薄れる。

実際の使用場面では関数呼び出しや他のオブジェクトのメンバとして使われると思うが、この方式では常にインターフェースとインスタンスを組にして扱わなければならない。 つまり関数呼び出しではインターフェースとインスタンスで引数は 2個必要だし、クラスのメンバにするときも同様にインターフェースとインスタンスのメンバが必要となる。

これは単に 2個ずつペアにするという手間だけの問題ではない。 もしインターフェースとインスタンスが別々のクラスのものを組み合わせたとき、悲劇が起こるだろう。たとえば、同じ UpDownインターフェースを実装した Integerとは全く異なる GuiSliderクラスがあったとして、次のような呼び出しを考えてみて欲しい。
setObject(Integer_interface_UpDown, guiSlider);  /* very bad! */

参考のため実行例も示す。
$ make clean test
cc -g -finstrument-functions -I. -I /export/home/kei/c/core/include -I /export/home/
kei/c/experimental/include -I /export/home/kei/c/io/include -I /export/home/kei/c/
lang/include -I /export/home/kei/c/message/include -I /export/home/kei/c/unit/include
 -I /export/home/kei/c/util/include -I /export/home/kei/c/x11/include -I ../../char/
include -I ../../class1/include  -c -o IntegerTest.o IntegerTest.c
# IntegerTest #
[CUnit] ./IntegerTest: testDown
[CUnit] ./IntegerTest: testGetValue
[CUnit] ./IntegerTest: testNewAndFinalize
[CUnit] ./IntegerTest: testUp
[SUCCEED] testDown in 2(ms)
[SUCCEED] testGetValue in 2(ms)
[SUCCEED] testNewAndFinalize in 3(ms)
[SUCCEED] testUp in 2(ms)
successes:    4 (100.0%)
failures:     0 (0.0%)
errors:       0 (0.0%)
total:        4 in 9(ms)
$ 


softies projectでのインターフェース

softiesでは、インターフェースはこの方法よりもより手の込んだやり方で実現している。それにより、前述の問題を解決し、インスタンスからインターフェースを取り出せるようにしている。

ただし、CUnit frameworkだけはこの例に近い方法を用いている。そうすることにより、softiesの設計・コーディング規約に従わない普通の Cのプログラムも単体テストできるようになっている。

babe 鯨

0 件のコメント:

コメントを投稿