株式会社エス・スリー・フォー

抽象データ型と Java/C++ そして COM/CORBA

抽象データ型とは…

  • 抽象データ型
  • 継承
  • 多態

をオブジェクト指向の三本柱などと称しています。その中でも抽象データ型(あるいはデータの抽象化)はオブジェクト指向の最も基本的で重要な概念ではないかと考えます。

"データを抽象化する"とは、データをそれに対して適用できる操作の集合で定義することです。

簡単な例として"カウンタ"を考えてみましょう。カウンタには3つの操作:

  • +1する (increment)
  • -1する (decrement)
  • 現在値を取得する

を提供させることにします。

さて、このカウンタをCで実現するとどうなるでしょうか…

typedef struct {
  long value_;
} Counter;

Counter* counter_create();
void     counter_incr(Counter*);
void     counter_decr(Counter*);
long     counter_value(const Counter*);
void     counter_release(Counter*);

/* ---- ここから実装部。Counterの利用者には見せない ---- */

Counter* counter_create() {
  Counter* c = (Counter*)malloc(sizeof(Counter));
  c->value_ = 0;
  return c;
}

void     counter_incr(Counter* c) { ++c->value_; }

void     counter_decr(Counter* c) { --c->value_; }

long     counter_value(const Counter* c) { return c->value_; }

void     counter_release(Counter* c) { free(c); }

…こんな感じになりますか。C++の入門書の中には、

データ(構造体) + アルゴリズム(関数) = オブジェクト(クラス)

と説明されたものもあります。つまり、構造体に、それに適用される操作を付け加えたものがクラスだ、というのです。

class Counter {
private:
  long value_;
public:
  Counter();
  ~Counter();
  void incr();
  void decr();
  long value() const;
};

/* --- ここから実装部。Counterの利用者には見せない --- */

Counte::Counter() : value_(0) {}

Counte::~Counter()  {}

void Counter::incr() { ++value_; }

void Counter::decr() { --value_; }

long Counter::value() const { return value_; }

Counterのメンバ変数value_は、利用者に勝手にアクセスさせないようprivate部に置きました。

カウンタの利用者にしてみればprivateメンバに何が定義されているかなんて(どうせアクセスできないのだから)知る必要もありません。+1できて、-1できて、値を取得できるということだけで十分です。

カウンタの実装者にしても同様です。+1する、-1する、値を返す、の3つの機能をきちんと実装できるのなら、privateメンバはその実現のためになら何を定義しようがかまわない。メソッドが呼ばれるたびに電話回線を通してはるか遠くにあるデータベースを更新・参照したっていいでしょうよ。

結局カウンタについて利用者と実装者との間できめておかなければならないのは、カウンタに対する操作だけなんです。

"Xに対して何ができるか"で定義したX、それが抽象データ型なんですね。

Javaにおける抽象データ型

Javaは、抽象データ型を直接サポートするキーワードinterfaceを持っています。

public interface Counter {
  void incr();
  void decr();
  int  value();
}

interfaceはclassではありません。実体が存在しません。Counterの実装者はinterfaceを実装(implements)するclassを作ります。

class CounterImpl implements Counter {
  private int value_;
  public CounterImpl() { value_ = 0; }
  // interface Counter を実装する
  public void incr() { ++value_; }
  public void decr() { --value_; }
  public int value() { return value_; }
}

利用者にCounterImplの存在を気づかせないために、Counterを生成するクラスCounterFactoryを用意しましょう。

public class CounterFactory {
  public static Counter create() { return new CounterImpl(); }
}

CounterFactory.create()はCounterImplをnewし、それをCounterとして返します。利用者にはCounterFactory.create()で手に入れたオブジェクトが実際にはCounterImplだということを隠しています。それでいいんです。利用者が欲しがっているのはCounterImplではなく、Counterに対する操作なのですから。

public class Client {
  public static void main(String[] arg) {
    Counter c = CounterFactory.create();
    for ( int i = 0; i < 3; ++i ) {
      c.incr();
      System.out.println("value= "+c.value());
    }
  }
}

Clientをアプレット化するとこんな感じになります。[1]

src

C++における抽象データ型

残念ながらC++ではJavaのinterfaceのような抽象データ型を直接にはサポートしていません。が、それと同等のことなら可能です。一切のメンバ変数を持たず、そして全メンバ関数を純粋仮想関数とするのです。

class Counter {
public:
  virtual ~Counter() {}
  virtual void incr() =0;
  virtual void decr() =0;
  virtual long value() const =0;
};

そしてこのCounterから派生したCounterImpl、およびCounterFactoryを作りましょう。

class CounterFactory {
public:
  static Counter* create();
  static void     release(Counter*);
};

/* --- ここから実装部。Counterの利用者には見せない --- */

class CounterImpl : virtual public Counter {
private:
  long value_;
public:
   CounterImpl() : value_(0) {}
   virtual void incr() { ++value_; }
   virtual void decr() { --value_; }
   virtual long value() const { return value_; }
};

Counter* CounterFactory::create() {
  return new CounterImpl;
}

void CounterFactory::release(Counter* c) {
   delete c;
}

利用者(Client)は以下のようになります。

using namespace std;

int main() {
  adt::Counter* c = adt::CounterFactory::create();
  for ( int i = 0; i < 3; ++i ) {
    c->incr();
    cout << "value= " << c->value() << endl;
  }
  adt::CounterFactory::release(c);
  return 0;
}

src

言語を越えて…

抽象データ型はその利用者と実装者との間の申し合わせです。これを発展させると、抽象データ型を利用者と実装者双方が認識できる、すなわち実装者はそのインタフェースをサポートするオブジェクトを生成し、利用者はそのインタフェースを提供するオブジェクトを手に入れて利用することができるなら、利用者と実装者が互いに異なる言語で書かれていてもいいはずです。それを実現したのがCOM、そしてCORBAです。

COM (Component Object Model)

Visual C++ 6.0 ATLを使ってCOMサーバ(実装側)を作ってみましょう。

Visual C++の”ATL COMAppWizard”を使えばCOMサーバをとっても簡単に作れます。

CounterのインタフェースICounterを作ってできたファイルadt.idlを以下に示します。

...
  interface ICounter : IDispatch {
    [id(1), helpstring("+1する")] HRESULT incr();
    [id(2), helpstring("-1する")] HRESULT decr();
    [propget, id(3), helpstring("値を返す")] HRESULT value([out, retval] long *pVal);
  };
...
  coclass Counter {
    [default] interface ICounter;
  };
...

adt.idlの中に、incr,decr,valueをサポートするインタフェースICounterと、ICounterをインタフェースとして持つクラスCounterがあることがわかるでしょう。

以下に示すのは最終的にできあがったCounterの実装部CCounterです。

class ATL_NO_VTABLE CCounter :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CCounter, &CLSID_Counter>,
  public IDispatchImpl<ICounter, &IID_ICounter, &LIBID_ADTLib> {
public:
  CCounter() { value_ = 0; }

DECLARE_REGISTRY_RESOURCEID(IDR_COUNTER)
DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CCounter)
  COM_INTERFACE_ENTRY(ICounter)
  COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

public:
  STDMETHOD(get_value)(long *pVal);
  STDMETHOD(decr)();
  STDMETHOD(incr)();
private:
  long value_;
};
/* --- implementation --- */

STDMETHODIMP CCounter::incr() {
  ++value_;
  return S_OK;
}

STDMETHODIMP CCounter::decr() {
  --value_;
  return S_OK;
}

STDMETHODIMP CCounter::get_value(long *pVal) {
  *pVal = value_;
  return S_OK;
}

src

これをコンパイルすればadt.dllができあがり、レジストリに登録されます。これでCOMをサポートする言語ならなんであれ、利用者はCounterを使うことができるはずです。

Visual BasicからCounterを使ってみましょうか。VisualBasicのメニューから”プロジェクト|参照設定”ることで、VisualBasicがCounterのインタフェースを認識します。

フォーム上にテキストボックス(txtValue)とボタンをふたつ(btnIncr,btnDecr)配置して:

Option Explicit

Private c As Counter

Private Sub btnDecr_Click()
  c.decr
  txtValue = c.Value
End Sub

Private Sub btnIncr_Click()
  c.incr
  txtValue = c.Value
End Sub

Private Sub Form_Load()
  Set c = New Counter
End Sub

Private Sub Form_Unload(Cancel As Integer)
  Set c = Nothing
End Sub

なんとまぁ、たったこれだけで動いてしまうんです。

CORBA (Common Object Request Broker Architecture

CORBAはインタフェースを利用者と実装者が共有するという考えをもう一歩すすめ、異なるマシン/異なるOS/異なるプロセス/異なる言語による利用者/実装者を可能にしてくれます。

Orbixを使ってCounterを作ります。まずはインタフェースの定義から。

module adt {
  interface Counter {
    void incr();
    void decr();
    readonly attribute long value;
  };
};

avaのinterfaceとそっくりです。このインタフェース定義ファイルadt.idlをIDLコンパイラに食わせると、利用者/実装者それぞれのためにスタブ/スケルトンを吐いてくれます。実装者はスケルトンを基にCounterを実装し、利用者はスタブを使ってCounterにアクセスします。

/*
 * interface Counter の実装
 */
#include "Counter.hh"

namespace adt {

  class CounterImpl : public virtual CounterBOAImpl {
  private:
    CORBA::Long value_;

  public:
    CounterImpl() : value_(0) {}

    virtual void incr(CORBA::Environment &IT_env=CORBA::default_environment) ;
    virtual void decr(CORBA::Environment &IT_env=CORBA::default_environment) ;
    virtual CORBA::Long value(CORBA::Environment &IT_env=CORBA::default_environment) ;
  };

}

namespace adt {

  void CounterImpl::incr(CORBA::Environment &IT_env) {
    ++value_;
  }

  void CounterImpl::decr(CORBA::Environment &IT_env) {
    --value_;
  }

  CORBA::Long CounterImpl::value(CORBA::Environment &IT_env)  {
    return value_;
  }

}

実装者(サーバ)は(COMをレジストリに登録するように)あらかじめOrbixデーモンに登録しておきます。利用者はデーモンの仲介でCounterを手に入れ、利用します。

/*
 * クライアント
 */
#include "Counter.hh"
#include <iostream.h>

int main() {
  adt::Counter_var c;
  c = adt::Counter::_bind(":adt");
  for ( int i = 0; i < 3; ++i ) {
    c->incr();
    cout << "value= " << c->value() << endl;
  }
  return 0;
}

src

注釈
  1. ^弊社のWebサイト公開ポリシーによりアプレットは削除しております。