読者です 読者をやめる 読者になる 読者になる

関数コールバックかインターフェイスか

 xoopscube.jpでのonokazuさんのこの書き込みはかなり的確なところをついている。XCの設計コンセプトとしては、onokazuさんが書かれているとおりです。ただ、onokazuさんがコールバック等を記述した設定リストくらいにしておいて、インターフェイス実装を避けた方がいいのでは、と書かれているのに対して、僕のほうからはインターフェイスないしは抽象クラスを使うかもしれない旨を返信しています。

 僕の主張は、たとえインターフェイス実装スタイルをとったとしても、それは書き方の問題でしかなく、モジュールとインターフェイス提供パッケージが"依存性の強いズブズブの関係"になってしまうことを意味しないというものです。ただし、確かにコールバックにすればその"見た目の関係"はもっとすっきりしたものになる。まして XC には仮想デリゲートが用意されている。

 にも関わらずインターフェイスクラスを作るかもしれないと書いたのはパフォーマンス上の理由によるものだ。とても面白い話なので、少し補足する。

 C言語C#のように「関数アドレス」をハンドリングすることができる言語では、モジュール間の接点を関数コールバックで行うことで、依存性を大幅に下げることができる。一方、関数アドレスがハンドリングできないJavaなどではインターフェイスにレスポンスするコードを実装して、それをコールバック発信主に登録するという方法が多用される。Listenerクラスが代表格と言える。

関数コールバック

 最近見かけた実例として、組込スクリプトエンジンのリモートデバッギングまわりの構成を挙げる。スクリプトエンジンのランタイムはターゲットマシンで動作し、PC上のリモートデバッガと接続して、互いにパケットを送り合っている。

  • VM モジュール
  • VM とリモートデバッガの間に流れる、メッセージコマンドと送受信データの仲介役を行う DebugSession モジュール(Debuggeeにちと近い)
  • ネット通信コードが記述されている NetClient モジュール


 この設計では、 VM は DebugSession を知らず、 DebugSession も NetClient を知らない。そもそも NetClient はスクリプトエンジンパッケージに含まれてもいなかった。

 これらのシステムはコールバックの接続で連携を行う。そのグルーコードはタイトルパッケージに含まれる AppScriptEngine の static メンバ関数に記述され、 init() で接続を行う形をとった。

 各モジュール間には依存性はないため、簡単に別物に取り替えることができる。「オーバーライドで拡張が容易」という表現ではなく「完全な別物と変更可能」になる。より高性能なリモートデバッガ資産があれば、それに対応した Debuggee を用意(移植)すればいいし、 NetClient はターゲットマシンのカーネルAPIベッタリのコードに差し替えることになるだろう。変更すべきはタイトルパッケージ所属のグルーコードだけだ。

 特に NetClient はスレッド動作することになるから、独立していることは有り難かった。この実例では、 DebugSession がメッセージをポーリングするコールバックと、送信したいメッセージを書き込むコールバックの2点があったので、 NetClient モジュールの同等のメンバ関数へ中継するグルーコードを記述した。

 外向けのロッキング(スレッドセーフ対応)も NetClient 内のわずか二箇所に留まっている。

 ローレベルの接続は、とてもC言語らしい書き方だが、C#でもデリゲートの用途としてお馴染みのやり方になっている。後述するが、もうひとつのやり方である「インターフェイス実装」は、単一継承しかできない言語ではステップがややこしくなることがある。C# などは単一継承ながらデリゲートが使用できるため、ケースバイケースでそれを使い分けることができるのが利点だ。

インターフェイス

 これもよく使われている方法で、Objective-Cのデリゲート(正確にはプロトコル)もこちらのタイプにあたる。インターフェイスを通じた通信は必ず同一のインスタンスに送信されるため、実装が非常に楽で、接続もインスタンスアドレスの引き渡し一発で済むというメリットがある。

 ただし、裏を返せば、関数ポインタやC#デリゲートのように接続の組み合わせが自由ではない。よって、インターフェイスに対し行われる通信を複数のモジュールで受け持つ構成を作る場合は若干手間がかかる。最低限として各モジュール間の独立性を保つにしても、ほぼアダプタークラスが必須になり、それを更にグルーコードから切り離そうとすると、2層3層の構造になってしまう点も若干面倒と言える。

 先のスクリプトエンジンの例を単純にインターフェイスを通じたコールバックに変更するならこのようになるだろう。

 ただし、これは C++ のケースで、 JavaC# では mix-in を使わない限り単一継承なので、もう1層間に入ることになる。

 といっても、基本的にはジャンプアドレスをどういう形で引くかの違いでしかない。関数ポインタを使って関数のアドレスを直接ハンドリングするのか、インスタンスを引き回す形でプログラムしておいて、関数アドレスの解決を vtable に委ねるかどうかの違いだ。基本的にはコードを書く側にとっては「表現方法の違い」の部類に入る。そういう意味では宗教戦争の誘因材かもしれない。*1

 先ほど書いたように、C#の場合はデリゲートとインターフェイスをケースバイケースで気軽に使い分けられるのから強力である。C++も同じだが、関数アドレスには型の問題が残りがちで、インターフェイス実装インスタンスの引き回しでは参照解除/メモリ解放タイミングがしばしばトラブルの要因となる。 JavaC# はリファレンスのメモリ管理があるからこのへんは気にしなくていい。

なぜインターフェイスを使うかもしれないのか

 XCにデリゲートを突っ込んだことからも分かるように、個人的にはローレベルのフィーチャーが好きだ。アダプターを挟まなくても各モジュールの独立性を保てる点からいっても、インターフェイス実装より、関数単位のハンドリングとコールバックのほうが、XCのコンセプトに向いている。*2

 しかし、パフォーマンスを考えると、PHPではインターフェイスなり抽象クラスなりを使わざるをえないのではないか。

 PHPではJava同様、関数がハンドリングできない。そこで文字列をパラメータにして、専用のコール関数を使ってコールバックを行う。文字列の時点では関数のシンボルは解決できていない。だから、関数のグローバルシンボルテーブル内を、文字列を使ってハッシュ検索して、都度シンボル解決してることになる。しかもその後関数オブジェクトを掴めるわけでもないので、コールのたびにこの負荷がかかる。

 一言で関数コールバックといっても、シンボル解決済みの素のアドレス情報をそのまま使って即ジャンプするC言語と、文字列からシンボルテーブルを都度検索するPHPでは、実行負荷は大きく違うものになる。そう、このエントリでは関数ポインタとインターフェイスを比較したが、関数ポインタなんてものはない。となると、判断は変わってくる。

 インターフェイスや抽象クラスにしておけば、それを実装したサブクラスのインスタンスのテーブルのメンバを検索すれば済むことになるから、ずっと軽いのではないか。

 コールバックの種類(機会)が増えれば増えるほど、パフォーマンス的にはインターフェイスのやり方のほうが有利になる。この負荷の違いが、種類×量に対してかかると考えると、どうせ同じことができるならインターフェイスのほうが良いのではないかと思えてしまう。それがインターフェイス実装スタイルを採用することになってしまうかもしれないと書いた理由だ。

 繰り返しになるが、アダプターを書くだけの話なので、これが独立性の高いプログラムモジュールを外部から突き壊す要因にはならない。

*1:見たことはないが

*2:この際 Object Oriented は忘れよう