なんぞこれ。コンストラクタパラメータが異常に大杉る・・・。バカなの?死ぬの?そういう場合はBuilderパターンを検討してみよう。
(追記:2009/11/18)
id:Nobuhisaさんにお返事を頂きました。ありがとうございます!
Effective JavaのアレをC#で - (hatena (diary ’Nobuhisa))
C#でBuilderを実装するにあたって、とても参考になりますので、こちらもぜひご覧ください。
引数大杉。Builderパターンだろう常考
異常にコンストラクタパラメータの多いクラスに直面して辟易することがある。そう、あるんです。そのうち必須パラメータが3個で、任意指定のパラメータが10個・・・。え、まじで?
驚きを禁じ得ない。。。作った奴は一体何を考えているのだろうか。
おそらく何も考えていないに違いない。なんてこと、あるよね?あるよね?
そういった痛いクラスを悪気もなく量産してくれちゃうような方には、
ぜひ、「Effective Java 第2版」にてJoshua Bloch氏が提案している、
「数多くのコンストラクタパラメータに直面した時にはビルダーを検討する」を読んで頂こう。
この本はその名のとおり実際Java使いによるJava使いのためのJava本なのだが、
Java使いではなくとも学ぶ事の多い良書なので、プログラマであればぜひ読んでおきたい。
このEffective JavaでJoshua Bloch氏が提案しているBuilderパターンは、
Pythonに見られる名前付きオプションパラメータを模倣してJavaで実装したたもので、
コンストラクタでのパラメータ指定をした場合と同様に、
そのパラメータに対する不変式を強制することができるというもの。
なので、コンストラクタやstaticなファクトリメソッドが多くのパラメータを
持つようなクラスを設計する場合、このパターン(というかイディオム)を適用するととても具合がいいというお話。
具体的には、生成したいオブジェクトのインスタンスをnewで直接生成する代わりに、
ビルダーオブジェクトを作って、生成したいオブジェクトが必要とする任意パラメータを
ビルダーに対して個別に設定しておいて、最後にビルダーに対して、
生成したいオブジェクトをインスタンス化する処理を委譲するような方法をとります。
C#でJoshua Bloch氏のBuilderパターン
C#はJavaとは言語仕様が当然異なるので、Javaと全く同じようには書けないが、このBuilderパターンをC#で実装する例を書きたいと思う。
いろいろな書き方があると思うが、自分は例えばこんな風に書いていたりします。
以下、C#によるBuilderパターンのサンプルコード。
using System; namespace ConsoleApplication1 { public class BuilderSample { private readonly int necessary1; private readonly int necessary2; private readonly int optional1; private readonly int optional2; private readonly int optional3; public sealed class Builder { public int necessary1 { get; private set; } public int necessary2 { get; private set; } public int optional1 { get; private set; } public int optional2 { get; private set; } public int optional3 { get; private set; } private Builder() { } public Builder Optional1(int val) { this.optional1 = val; return this; } public Builder Optional2(int val) { this.optional2 = val; return this; } public Builder Optional3(int val) { this.optional3 = val; return this; } public BuilderSample Build(int necessary1, int necessary2) { this.necessary1 = necessary1; this.necessary2 = necessary2; return new BuilderSample(this); } public static Builder Instance { get{ return new Builder(); } } } private BuilderSample(Builder builder) { this.necessary1 = builder.necessary1; this.necessary2 = builder.necessary2; this.optional1 = builder.optional1; this.optional2 = builder.optional2; this.optional3 = builder.optional3; } public override string ToString() { string result = ""; result += this.necessary1.ToString() + ":"; result += this.necessary2.ToString() + ":"; result += this.optional1.ToString() + ":"; result += this.optional2.ToString() + ":"; result += this.optional3.ToString(); return result; } } }
BuilderSampleクラスのコンストラクタがprivateである点に注目です。
今回は、Buildメソッドで必須パラメータを受け取ることにして、
builder インスタンスを使いまわして必須パラメータの決定を遅らせる風味にしてみました。
Builder内の各プロパティのgeterがpublicなので、外に見えてしまっているところが
少々不格好ですが仕方ありません。もっと良い書き方があれば教えてください。
で、上記のようにBuilderパターンで実装しておくと、下記のようにインスタンスを生成することができます。
using System; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { var sample1 = BuilderSample.Builder.Instance.Optional1(100).Optional3(35).Optional2(27).Build(100, 204); var sample2 = BuilderSample.Builder.Instance.Optional3(33).Build(450, 56); var sample3 = BuilderSample.Builder.Instance.Build(10, 10); Console.WriteLine(sample1); Console.WriteLine(sample2); Console.WriteLine(sample3); Console.ReadKey(); } } }
実行結果
100:204:100:27:35 450:56:0:0:33 10:10:0:0:0
多くの任意のパラメータが必要になるようなケースであれば、
すべてをコンストラクタパラメータで渡してしまうよりも、間違いなくこのパターンを採用した方がうまくいくでしょう。
非常にシンプルでわかりやすいイディオムなので、すぐに覚えられると思います。
そもそも引数大杉とか不吉な臭い充満なわけで
OCamlの統計に、こんなのがあるんです。
OCamlのライブラリの2239関数のうち高階関数を使ったものがそもそも12.8%だけ。
関数引数を1つだけ取るものが12.1%で、2つ以上の関数を取るものは0.7%しかないというんです。
というような統計もあるくらいなので、まともな設計をしていれば、
そもそも関数の引数はせいぜい2、3個程度で間に合うことが殆どである。というのは事実でしょう。
なので、確かにBuilderパターンはこのような問題を解決するための良いテクニックのひとつではあるのですが、
盲目的に何でもかんでもBuilderパターンを適用しとけばおk!というわけでは当然ありません。
例えば引数を4つ以上とるような関数があった場合、何かしら不吉な臭いがないか疑いを持つ必要はあると思う。
「そもそもそんなにたくさん引数いるんかい?いらないんじゃないかい?」と。
そんなときは、関数ひとつひとつについて再度見直すのもいいんじゃないかなあ、なんて思ったりする今日この頃です。