読まなくても良い前口上 §
これまで、汎用のライブラリというものには、あまり手を出してきませんでした。なぜかといえば、それを設計することは極めて難しいからです。より具体的に言えば、未来は未知であり、未来の時点で必要とされる機能性を予測することは事実上不可能であるからです。
それゆえに、1つのプログラムに含まれるクラスやコードは、そのプログラムを実現するための必要な最低限のコードのみを含む……ということを理想として(それを達成したかどうかは別として)やってきました。今そこにあるプログラムに対して最善であるかは、ある程度判断できるからです。
とはいえ、そろそろそれだけでは済まなくなってきました。
実はさる理由で、りすと亭の簡易httpd機能を切り出して汎用ライブラリ化を行っています。
基本的に、凄い機能性も凄い拡張性も要求していません。ただ、りすと亭から完全に分離され、単独で別のプログラムに取り込めれば良いだけです。
ところが、たったそれだけのことが、なかなか難しいのです。
当初は、継承ベースで拡張可能にするというオーソドックスな構成を取ってみましたが、その場合スマートに扱うには多重継承が不可欠という結論になりました。もちろんC#に多重継承はないので、その選択はアウトです。
というわけで、ソースを書き換えつつ検討してみました。これは暫定的なメモです。
前提 §
プログラム言語はC# 2.0。実行環境は.NET Framework 2.0を前提として考えます。
また、あくまで「私流」の書き方に関するメモであって、世間一般の世界にはこれとは異なるやり方が多数あり、それらが間違っているという話でもありません。
あくまで、私がしっくり来るスタイルを考えてみよう……という話でしかありません。
また、コードを書きながら考えた暫定的なメモなので、正しいという保証はありません。むしろ間違っていると思って読むのが賢い態度でしょう。
拡張可能なライブラリとは何か §
単一または複数のクラス等を含んだライブラリであり、ライブラリに対する呼び出しに対する動作の一部をライブラリ本体を書き換えることなく変更することができるもの……とします。
つまり呼び出しの結果、常に同じ結果を返すライブラリ(例: NestedHtmlWriter)は当てはまりません。
ライブラリの変更とは何か §
とりあえず、大ざっぱに以下のようなものがあり得ます。(全てとは限らない)
- 「拡張」 クラス等に新しい機能や情報を追加する
- 「差し替え」 メソッド等を必要に応じて差し替え可能とする
- 「抽象化」 具体的なクラスではなく、抽象的なクラスを受け渡すようにする
- 「コールバック」 外部の情報を得るための手段をコールバックするメソッド等として実現し、いつでも動的な値を参照可能とする
- 「実装」 ライブラリ側でデフォルト動作を提供できない機能はinterfaceのみを定義し、ユーザープログラム側で実装する
しかし、最後の3つは「差し替え」の一種と考えられます。
実はライブラリの変更は「拡張」と「差し替え」の2種類に集約して考えられます。
この2つを実現する適切な方法を持つことが、ライブラリの変更に備えることになります。
「拡張」の実現方法 §
ライブラリに含まれるクラス等に、ライブラリを変更することなく情報や機能などを追加します。
これを実現する方法は、おそらく「継承」以外にありません。
「差し替え」の実現方法 §
ライブラリに含まれるメソッド等の機能を、ライブラリを変更することなく差し替えます。
これを実現する方法は多数あります。
- 継承によって抽象/仮想メソッドをオーバーライドする
- interfaceを実装することで実現する
- eventを用い、イベントハンドラとして機能を登録する
- delegate型のメンバ変数を用い、これを書き換えることで機能を差し替える
まず、継承を使った方法は、多重継承を要求されることがあるので不適切という結論になりました。
次に、interfaceの実装は、デフォルト動作を提供できないという問題があります。たとえば、3つのメソッドを提供するinterfaceがあるとして、特別な挙動を必要とするのはそのうちに1つに過ぎないとしても、3つのメソッドを実装しなければなりません。このオーバーヘッドは避けたいものです。
次に、eventを使った方法は、イベントハンドラの引数に直接自由な値を渡せないという制約があり、ソースコードの可読性を落とします。
最後の方法には、とりあえず他の方法に見られる短所はありません。しかし、定義が階層化したときに継承ほど上手くそれをさばけないという問題があります。(スーパークラスの同メソッドを呼ぶ……というような機能を容易かつ安全に実現できない。delegateにはスーパークラスという概念そのものが無縁)
しかし、他に方法はないので、定義の階層が深くなることはない……という前提を置いて最後の選択を取ります。
delegate型のメンバ変数を使うメリット §
普通のメソッドと全く書式が異なるため、仮想メソッドと比べて「差し替えられる可能性がある」ことがソースコード上で分かりやすくなります。
匿名メソッドの扱いやすさの問題 §
delegate型のメンバ変数を差し替える場合、そこには常に「差し替えるコード」と「差し替えられるコード」の2つが必要とされ、それは1つのメソッドでは済まされません。仮想メソッドのオーバーライドなら1つのメソッドを書くだけで差し替えられるにも関わらず、2つのメソッドを要求するのは手間の増加を意味します。
しかし、「差し替えられるコード」を匿名メソッドとすることで、この手間を最小化することができます。
つまり、もし匿名メソッドが使えないC# 1.0が前提なら採用しなかったかもしれない選択といえます。
(当初は嫌いだった匿名メソッドに、こうして慣らされていくのね……)
「拡張」と「差し替え」が同時に行われるとき §
実は、このやり方でクラスを設計する場合の最大の罠は、「拡張」と「差し替え」が1つのクラスで同時に要求されるケースにあります。
「拡張」を実現するためにクラスの継承を行ったとき、実はその継承によって「差し替え」も同時に実現できてしまうのです。「拡張」と「差し替え」を異なる性質を持った全く異なった作業であるという確固たる認識を持たないで作業を行っていると、全てを「継承」で済ませるようなコードを書いてしまいかねません。(というか、それが数日前の私だ!)
しかし、そのようなクラスを使う側を書くときに、はたと綺麗に書けないという現実に直面する可能性があります。
従って、「拡張」と「差し替え」の双方を行うクラスであっても、継承によって実現するものは「拡張」に限定し、「差し替え」はdelegateを用いて行う方が良い……と考えられます。
感想・ああ多重継承! §
多重継承が使えれば、残された問題のうちのいくつは完全に解決します。
しかし、単純に多重継承を持ち込むと、別の問題群がセットで付いてくるので、それもまた悩ましい問題です。
問題を解消したエレガントな多重継承は可能ではないか……という着想を得てからだいぶ経ちますが、それについての考えはさっぱり進んでいません……。
それもまた悩ましい……。
余談・どうして、こんな文章を書いたの? §
作業中に混乱しないよう、自分の考え方を明確化するためです。