VCLのつくり…別題「我ながらよくもまあこんなにだらだらと書いたもんだ」

経過。
http://d.hatena.ne.jp/lethevert/20050829/p5
http://d.hatena.ne.jp/lethevert/20050831/p3
http://d.hatena.ne.jp/soutaro/20050901/1125614439
http://d.hatena.ne.jp/lethevert/20050902/p1
http://d.hatena.ne.jp/soutaro/20050901/1125621086

  • VCLのTTreeViewはinterfaceを使って既存の木構造を表示するようになっているべきだ(lethevertさん)
    • VCLコンポーネントは、シリアライズのため、構成オブジェクトを所有してる必要がある(YT)
      • そのレベルでデザインセンスが微妙(lethevertさん)
        • VCLシリアライズ第一で作られているのでそこに文句をつけてもしょうがない(YT)
      • interfaceを使ってもシリアライズ可能では?(lethevertさん) ★1
      • 別にコンテナコンポーネントを作ってそれを表示すればいい(lethevertさん)
    • コンストラクタも多態するためCreate(Owner: TComponent);の形から変えられない(YT)
    • TStrings等もinterfaceを使うようになっていたらデータコピーの必要が無い(lethevertさん)
      • VCLは所詮OSのGUI部品へのラッパーなので二重管理をすると色々問題(YT)
        • interfaceを利用すればコピーを行わなくてもよいのでメモリの節約になる(lethevertさん)★2
      • interfaceなら既存のデータ構造に少し加えるだけで見せることができる(lethevertさん)
        • 表示したいGUIコンポーネントのためのinterfaceをオブジェクトに持たせておくのは不自然(soutaroさん)
          • NodeはGUIのためのinterfaceではなくツリーアクセス一般のためのもの(lethevertさん)
            • そういう「一般的な」interfaceは不可能(soutaroさん)★3
      • TStringsはデータ本体の格納方法が異なるものを同じに見せるためでTTreeNodeにもTStrings同様の抽象基底クラスは必要(YT、横道)
      • Swingとは異なり同じJavaでもOSのコントロールを使うSWTVCL同様項目を後から生成する設計(YT)

(要約、言葉はシリアライズで統一)

いつの間にやらsoutaroさんが割り込んでくれてなにやら的確っぽい切り口をしておられる。
コメントではなくこんなところから失礼しますが、まずsoutaroさんへ。Delphiの型システムは、おっしゃられるとおり、OOP対応部分に限定すれば、総称実装以前の(1.4以前の)Javaとほぼ同じといっていいと思います。細かい差異としては、interfaceからclassへのダウンキャストができないとか、委任と呼ばれる丸投げ委譲構文があるとか、RTTIがpublishedに限られるとか、静的なメタクラスがあるとか、GCが(.NETターゲットを除き)無いとか、危険なキャストが可能でしかも一般的に使われてたりするとかとか、まあそんな程度です。ただ、VMが無いという事実も加算すると、この差異は結構でかいです。
強い味方ができたっぽいので、理屈の上の話はsoutaroさんに任せて(こら)、私は泥臭い実装上の話を…。

あとlethevertさんにひとつ訂正をば。

さっきのデータオブジェクトを、TTreeNodeオブジェクトのサブクラスとして作り直す

T(Custom)TreeViewの派生クラスを作らない限りこれは不可です。あ…コンストラクタもTCustomTreeView.CreateNodeも非仮想ですので、一切不可か…と思いきやOnCreateNodeClassなんてイベントを発見。ごめんなさい。…って、Create非仮想なのに…!?いいのかこれ…。

★1…まずはDelphiの話。
面倒であれば最後の一行だけ読んでください。

とにかく、インターフェースというのは、形式さえ整えれば、実装は問わないというところに醍醐味があるので、表示したい元のデータがどういう形をしていても、必要なメソッドさえそろえれば、描画コンポーネントには何も手を加えずに描画できるということを主張したいのです。つまり、インターフェースを使っている方が、データと描画がより適切に分離された形でコーディングできるということです。

このインターフェースのインターフェースたる能力のためにシリアライズができないのですよね…。
Javaでしたら、Javaの文法で宣言されたinterfaceの実体は、絶対にJavaのclassです。VMは完璧に実体を把握しており、何をしようが自由自在です。
一方VMを持たない言語のinterfaceは、単なるVMTへのポインタを先頭とするメモリブロックへのポインタです。gccですとVMTの位置が違うのでCOMを呼べない等コンパイラ毎に細かい違いはありますが、少なくともDelphiのそれはCOM完全互換です。そして、interfaceは、普通に言語間の橋渡しに使われたりもします。
実体は.dllの先にあるかも知れませんし、まったく別のコンパイラで書かれているかもしれませんし、私がたまにやりますようにメモリ上に関数ポインタを並べたものをキャストして突っ込んだだけでもそれはinterfaceとして動作します。
一方VCLシリアライズは、コンパイラがclass毎に生成するRTTIを使って、第4のアクセス制御であるところのpublishedに公開されたプロパティに動的にアクセスすることで実現されています。
つまりコンパイラはclassについては把握していますがinterfaceの先は何も知りません。
interfaceからはRTTIが取れないのです。ISerializableの類を実装しても仕方のない話で、再生の方向では、オブジェクトの実体を生成しないといけないため、実体が、TComponentにしろTCollectionItemにしろ、VCLが既知の、しかも仮想コンストラクタを持ったclassから派生していなければなりません。しかし、それを知る方法はありません。コンパイラの埋め込んだディスパッチコードをトレースすれば可能なんて話は置いておいて。interfaceの実現方法は何でも良いため、下手な探りを入れるとアクセス違反になっちゃいますしね。いやDのinterfaceはinterfaceからclassへダウンキャスト可能とかそういう話も置いておいて。
いや私も思うんですよ、必ずVMTの先頭にあるQueryInterfaceに対してマジックナンバーを要求する等で確認可能なのではないか?って…あ、それでも実装先が同コンパイラコンパイルされてなおかつ.dllにあった場合が厄介か…いやまてよそもそも.dllの場合はRTTIも.dllにあるはずで…まあそんな感じです。
しかも実はこれらは私が考えた後付の理由に過ぎません。
ホントの理由としては、TStringsはDelphi1の頃からあって、TTreeViewがVCLに追加されたのがDelphi2で。当時はinterfaceなんてものは、丁度Delphi1と同じぐらいの年に発表されたJavaという遅くてしょうがないアプレット用言語が持ち込んだ劣化多重継承でしかなかったわけで。いやひょっとしたらそれ以前に同様の機能を持った言語があっても何も不思議は無いのですが、私が高校生のときに読んだ雑誌ではC++で菱形継承をどう解決するかなあなんて話をしてたと記憶しています。Eiffelですと多重継承は全て菱形継承になるため名前の解決などを細々指示できるようになっていて云々。結論として菱形にならないmix-in継承が推奨されてたのは記憶してますが。つまるところ、当時のDelphiにはinterfaceなんて無かった、これが最大の理由でしょう。
interfaceそのものはCOM対応でDelphi3にて追加され、その後Delphi4でimplementsが追加され、以降もIInterface等が整備され続け、今やDelphiは、参照カウンタの問題を除けば、interfaceを最も素敵に使える言語となりました。
完璧余談ですが、Delphiの持つメソッド解決節、あれがないと、interfaceの持つメソッド名が、全てのinterface間でグローバルなものになってしまい困るという話を、少し前にcomp.lang.adaで見かけたような。どこでしたっけね。まあいいか。実際問題interfaceのメソッド名が名前の一等地を確保してしまっているなんてのはよくある話ですから、AとBを実装しないといけないのにこのふたつのinterfaceが両方ともCというメソッドを同シグネチャで要求しているなんてことも起こりえます。私が自分ひとりで書いていてもExecuteだのは衝突しますから、そんな時メソッド解決節は便利です。Java使いの方ですと無意識のうちに回避しているかもしれませんので、こんな話はむしろプロトタイプベースのスクリプトユーザーの方なら同意していただけないでしょうかね。閑話休題
あー、なんだ。要するに実装順その他諸々によりVCLではinterfaceはシリアライズ対象外です。最初から動的配列があればTListなんて要らなかったのと同じです。

★2…次にWindowsの話。
VCLに限らず、SWTでもそう云々は、Windowsのコントロールの実装からして、lethevertさん書くところの、次のようなコードになってしまっているのですよ。

class Node
{
  void setData(String label, Object obj) {...}
  String getLagel() {...}
  Object getObject() {...}
}

class TreeView
{
  Node rootnode;
  TreeView()
  {
    super();
    this.rootnode = new Node();
  }
  ...
}

しかも、コントロールを使う側は、Nodeへのポインタ参照の類は、決して扱うことができません。IDだのインデックスだので間接的に取り扱う必要があります。これは、むしろ(共通言語ランタイムのようなものが無い環境で)コンパイラを問わずに相互呼び出しを行う時のマナーであり、今も原則変わらないと思います。ただ、COM互換interfaceの類は解放を含む操作関数をVMTに含めてどこからでも呼べるようにしてますので、柔軟化はしてます。
問題は、これをラップするclassが、これを外界に見せる方法として何が適切か、ということですね。
Swingですと、全てJavaの世界ですから、コントロールは与えられたツリーを使い続ければそれでいいです。しかし、コントロールの実装が外部にあると、lethevertさんの方法では、必ずコンストラクタで全ノードのコピーが要求されます。その後も、ユーザーが作成したデータツリーは生き続けます。VCLSWTの方法では、ノードに対応するラッパーオブジェクトが生きているのは、ユーザーが各ノードを作ったりアクセスしたりする間だけでいいのですよ。用が済んだらラッパーは破棄してしまっても、コントロール自体はノードを覚え続けていますし、また必要があれば何食わぬ顔でラッパーを再作成してユーザーの問い合わせに答えればいいです。構造に変更があったときの同期の心配も要りません。
そうそう、この手のもので二重管理がされていると、同期を取って整合性を保つのは非常に難しい話となります。外部のコントロールはいわば融通が利かないスーパークラスですので、場合によっては必要な通知を全部カバーし切れなかったりします。しかもノードはどこから飛んでくるかわからないSendMessage(大抵、ユーザーが、lethevertさんのおっしゃるように効率化目的や、ライブラリがラップし切れてない機能に触れるために飛ばす)によって容易に増減し得ますし、そんな理由でデータツリーの側を直さないといけないかといったら絶対違うでしょう。逆にデータツリーのほうが更新された場合、それがユーザーの手によって正しくコントロールへと通知されないままコントロールがそれを使いつづけると、矛盾が出そうなのも容易に想像つきますし。
ユーザーへ二重管理を破綻させるラップ先の外部コントロールへの生アクセスを許している以上は、同期の手間は、ユーザーに押しつけるべきというか…
発想が逆なんですよね。
Windowsコントロールはそれ自体がデータストレージでもあるのですから、それを一次データとして、データの格納先がどこであっても構わないようなinterfaceを用意する方が、ラッパーとしては安心できる実装でしょう。
TStringsはまさにそういう抽象基底クラスですし、TTreeNodeにも同様のものが必要なはずなのに存在していないという指摘ならその通りなんですよ。TTreeView以外にまともなツリー構造を扱うコンポーネントが(Win3.1タブの互換用コンポーネントを除いて)VCLには標準添付されていないという事実がこれを覆い隠してしまってますし、話も逸れてってますし…。
更に、実際にデータをツリービューに表示するために必要なinterfaceは提示のNodeだけでは不足するのではないでしょうか。

Windowsのコントロールは実はあまりよく理解していないですが、保持する必要のあるデータって、文字列と関連データへのポインタくらいだと思うので

Windowsのツリービューは意外と高機能で、各ノードはイメージリストからビットマップを切り出して表示することもできれば、チェックボックスだって持てます。オーナードローだってできちゃいます。インプレース編集もできますし、ポイントすればバルーンヒントも浮かびます。
そういうのを使いたいとなったら、各機能に対応するinterfaceを各Nodeが実装するわけですか?それならTTreeNode(がラップしてあるWindowsコントロールの内部データ)に任せてしまったほうがずっとマシに思えます。一般的なsetCheck,getCheckを行うinterface、なんて称してもそりゃ通りますが誰も使いたくないと思いますよ…。

★3…一般的なインターフェースの話。
私には思いつかなかった切り口です。
この点は、IList, ICollectionを実装してくれるC#の配列みたいに、言語が定義して半ば強制しているinterfaceであれば、その言語限定で、一般的なinterfaceになりえるかもしれないとは思います。ただ、それは、ある種の退化のような気もしますし、複数言語を連携させるときは悩みの種にもなり得ます。
素人考えでは、言語がデータ構造へのインタフェース(interfaceではなくシグネチャの塊としてのインターフェース)を規定してしまっていると、その上でできる範囲ではとても幸せで、道を踏み外すととたんに地獄が待っているような印象ですね。連想配列を持つ言語で素直な文字列の連想配列は楽でも、Windowsのファイル名みたいな大文字小文字を同一視しつつ最初に与えられた文字ケースは持っておかないといけない連想配列は一瞬考え込んでしまうように。(こんな例は比較演算を外から与えることができればそれだけで解決してしまいますので適当ではないかも)
言語以外の、特定のライブラリが、一般的なinterfaceを主張するような状況は、乱立と、ライブラリと無関係なデータがなんでライブラリに従わないといけないのだって気分になるのが目に見えてますのでやらないほうがマシです。
まあ、なんですか。getLabelというか、toStringというか、文字列が人間に可読という理由だけで、なんでNodeにしろObjectにしろ基本的なinterfaceや基底クラスと謳われているものが、こんなもの持ってないといけないのかってのもたまに思いますので、soutaroさんのおっしゃられるように本当は一般的なinterfaceなんて存在せず、言語に騙されてるだけかもです。ちなみにtoStringではなくtoDebugStringという名前であれば非常に納得してしまいそうな私もいます。
というかですね、Swingの場合どうなってるのか思って見てみれば、lethevertさんの言うNodeにあたるTreeModelと、あとはイベントリスナでごにょごにょやるようになってるじゃないですか。で、addTreeModelListenerなんてある時点で一般的なinterfaceと言い難いような気も、というより必要ですよねやっぱり専用interface。宣言されているパッケージからしてSwing専用ですし。
ツリービューなんて複雑なコントロール、単純にいくわけないのですから…。

そして、果てしなく横道へ。
最近の流れとして継承が避けられてきている、らしい、にも関わらず、VCLの継承構造が馬鹿みたいに深いのは、「全てis-aなのだからしょうがない」の一言に尽きると思います。継承の変わりに委譲やらそういう話は、is-a関係を持たないものにまで継承を使うなという話で、どこからどう見てもis-aなら継承が自然。Windowsのコントロールのラップなんて巨大にならざるを得ない実装が引っ付いてくる場合は特に。

TMySpecialTreeView is-a TTreeView is-a TCustomTreeView is-a TWinControl is-a TControl is-a TComponent is-a TPersistent is-a TObject

どこにもis-aをhas-aにして流れをぶった切る余地なんて見つけられませんが、私染められてますかね。TControlなんて半分実装半分override前提の仮想関数なのですから、こんなの委譲で再利用なんてimplements(委任)使ってOKとしてもやっとれるかーになるのは目に見えてますし。
SetWindowLongによるウィンドウプロシージャ差し替えのベタベタな差分プログラミングは、++ではないCがメインで、F-BASIC for Windowsなんてものが生きていた時代から、上手く機能してましたからね。
http://d.hatena.ne.jp/pmoky/20050829に結構同意と言いますか。楽をするための苦労ならまだしも、素直に継承すべき場面を無理やり委譲なんてのは、記述量が増えて.exeサイズも増えて後々メンテもし辛くなってといいことないです。苦労をするための苦労は…
…と書きかけて、この長文はまさに苦労をするための苦労だよなあ、と、はたと。

うーむ、二日もかけて考えた割に全然まとまってませんな…。
そして、書き終えてから、lethevertさんはSwingを想定して話をしたわけではなく、私が勝手にSwingの話だと思ったことに気付いてしまったこの事実をどうしてくれよう…。

同期のところに追記。http://d.hatena.ne.jp/pmoky/20050902#1125626014、.NETのWindows.FormsのListBoxではIListがセットされた場合、リードオンリーにして同期の問題をかわしているらしいです。