@RSan からのリクエスト。大した内容ではないかもしれないけれども書いてみる。どうも表示が変になるので不自然な (C++ 的な?w) スペースを置いてます。
都合上コードを色々参照してるけど、全て MIT License が適用されてます。まあ見られるくらいどうでもいいけれど。
前説
アプリケーションドメインをラップするコードを書いていたわけだが、AppDomainSetup.ConfigurationFile を設定する必要に駆られた。
順当にやるならばコンストラクタ (このコンストラクタは最終的に AppDomainSetup を作って低レベルのコンストラクタに引き渡している) に引数を追加してやればいいわけだが、そんなことをやってはきりがないのは確定的に明らか。そもそも「次」の引数候補がいつ出るか分からない。抜本的な思考の転換が必要とされていたわけ。
そこでふと思い出したのが、自分がかつて書いた HttpWebRequest をラップするコード。要は Get とかする度にうまいこと HttpWebRequest を裏で作ってくれるわけだけど、この HttpWebRequest をジェネレートする部分を
public Action< HttpWebResponse > ResponseHandler { get; set; }
として公開しているわけ。これを応用しようと思いついた。以上。
Action<T> in constructors
とりあえず最初にコードを出してみる。…と思ったけど例のコードは色々サンプルにならない部分があるので新しく書き起こそうと思う。
例として ProcessStartInfo を挙げた。後は自分の例として HttpWebRequest、AppDomainSetup、の他に CompilerParameters とかでも適用可能だと思う。
変態なクラスしか頭に浮かばないけどご愛敬。まだまだあるはず。ランタイム外でも。
public class SampleClass
{
public SampleClass(ProcessStartInfo info)
{
// ...
}
public SampleClass(
String name,
params Action< ProcessStartInfo >[] infoInitializers
)
: this(_Apply(new ProcessStartInfo(name), infoInitializers))
{
}
private static T _Apply< T >(T obj, IEnumerable< Action< T >> initializers)
{
foreach (var f in initializers) f(obj);
return obj;
}
}
変なヘルパメソッド作らなきゃいけないのが癪。自分のコードでは怪しげな自前拡張メソッドで済ませているけど。let 欲しい。
つかいかた。
new SampleClass("foo.exe", info => info.Arguments = "123");
new SampleClass("foo.exe",
info => info.Arguments = "123",
info => info.UserName = "User"
);
new SampleClass("foo.exe", info =>
{
info.Arguments = "123";
info.UserName = "User";
});
おまけ
params Action< T >[] だけでなく、IEnumerable< Action< T >> なオーバーロードもお好みで。LINQ との親和性の問題。params IEnumerable< T > 欲しい。
解説
これは簡単に言ってしまえばオブジェクト初期化子的な柔軟性を与えるわけだけれども、この設計のポイント的な部分を数点。
なぜオブジェクト初期化子ではないのか?
なぜ
new SampleClass(new ProcessStartInfo("foo.exe")
{
Arguments = "123",
UserName = "User"
});
ではなく
new SampleClass("foo.exe",
info => info.Arguments = "123",
info => info.UserName = "User",
);
なのか。詰まるところ、オブジェクト初期化子は累積的な設定が不可能。つまり、引数とユーザ名のみを独立した引数としたコンストラクタを用意したとき (ここで最後に params Action<ProcessStartInfo>[] を用意するのがミソ)、つまり:
public SampleClass(
String name,
String args,
String user,
params Action< ProcessStartInfo >[] infoInitializers
)
: this(name, info =>
{
info.Arguments = args;
info.UserName = user;
})
{
}
というコンストラクタを置いたときに、さらにパスワードまで設定したい時に、この設計なら
public SampleClass(String name, String args, String user, SecureString pass)
: this(name, info, info => info.Password = password)
{
}
とできる。オブジェクト初期化子を使った設計の場合は、
new SampleClass(new ProcessStartInfo("foo.exe")
{
Arguments = args,
UserName = user,
Password = pass,
});
といちいち同じ事を書かねばならない。新しいコンストラクタのオーバーロードを作って引数の少ない方から多い方へ流してやればいいというのは問題ではなくて、ここでの問題は、この設計によって、どんな設定であってもオーバーロードを必ずしも作ることなく対応することができ、また作ったとしても更なる設定でも対応可能だということである。副次的に、メソッドの呼び出し (Set~(obj) 等) も可能となる。
params であるということ
上で
public SampleClass(
String name,
String args,
String user,
params Action< ProcessStartInfo >[] infoInitializers)
というシグネチャを示したが、ここで、このオーバーロードは
new SampleClass("name", "args", "user")
という呼び出しでも当てはまる。params は空の引数を受け入れる。よってユーザはこの追加のイニシャライザ引数を (オーバーロードを別途用意することなく) 意識することなく使用できる。地味に見えて結構大きいポイント。
他には?
あとは…
- 単純な関数オブジェクトのシーケンスなので、どこかの static フィールドに設定を用意しておいて、それを連結したり、あるいは、設定の記述である IEnumerable<Action<T>> を生成する関数を用意したり、と、LINQ とかメタ的なプログラミングの手段も大いに援用できる。
- 順序を伴った動作のカプセル化なので、(特に継承元のクラスのコンストラクタを呼ぶ場合) 既に設定された値を上書きするといったことも可能。
まとめ
オブジェクト初期化子も便利だけど、設定項目が多く、また何が設定される可能性があるか予測が困難であるオブジェクトの初期化はこのパターンを適用するのも面白いんじゃないの?
本稿ではコンストラクタに適用する例を挙げたけど、普通のメソッド設計でも十分有用に思う。