連載:[完全版]究極のC#プログラミング

C#3.0 デザインパターン ミニカタログ

川俣 晶
2010/06/07
Page1 Page2

 このミニカタログは、筆者がC# 3.0でプログラミングを行っていて効能に気づいたパターンをまとめた。主に本文で紹介したものを取り上げているが、解説が本文中に分散しているので、要約としてこのカタログにまとめたものである。

 このミニカタログは実際に動作するソースコードより見い出されたパターンから構成されているので、個々のパターンの有効性は証明済みである。ただし、「銀の弾丸」は存在しない以上、どのパターンも万能ではない。すべてのケースにおいて有効ではありえない。「私のケースではうまく機能しない」と思ったら、使用を取りやめることも重要である。その場合でも、このミニカタログはC# 3.0を使っていくヒントにはなるだろう。

■カスタマイズ可能なメソッド

●目的

  • メソッドの動作を呼び出し側できめ細かく制御する
  • 呼び出し側に固有の処理は、呼び出し側に記述する
  • メソッド本体を書き換えることなく、新しい処理のバリエーションを追加できる可能性を高める

●動機

  • 呼び出す側の違いによって処理を分ける必要のあるメソッドは見通しが悪くなる。これをすっきりさせたい

●適用可能性

  • メソッドに条件判断があり、呼び出し側の相違によって処理を分けている場合

●構造

  • メソッドの引数にデリゲート型の値を追加し、呼び出し側に依存するコードはそれを通して渡す。実行する処理が存在しない場合は、nullデリゲートを渡す

●実装例/サンプルコード

●関連するパターン

  • nullデリゲート

■遅延実行

●目的

  • ある処理を、いますぐではなく後から実行させる

●動機

  • ある処理を行う必要が確定するタイミングと、それを実行すべきタイミングに差があるとき、この時間差をスマートに乗り越えさせたい

●適用可能性

  • 処理を行うべき内容がラムダ式などによりデリゲート型として記述可能であり、実行すべきタイミングでそれを呼び出せる場合

●構造

  • デリゲート型の変数に実行すべき処理を記述したラムダ式などを代入しておき、実行すべきタイミングでそれを呼び出す。デリゲート型の変数を宣言する際は、nullデリゲートを初期値として指定してもよい

●実装例/サンプルコード

●関連するパターン

  • nullデリゲート

■nullデリゲート

●目的

  • 処理すべき内容が存在しない場合に、何もしないデリゲート型の値を渡すことで呼び出し側の条件判断を除去する

●動機

  • デリゲート型で実行すべき処理を渡す構造を取る場合、しばしば実行すべき処理が存在しないことがある。この場合、nullを渡してnullの場合は呼び出さないという処理を記述するのが最もストレートであるが、本質とはあまり関係のない条件判断なので、できれば取りたい

●適用可能性

  • 処理内容をデリゲート型で受け渡している
  • 処理すべき内容が存在しないケースがある

●構造

  • 以下のようなラムダ式を記述し、これをnullデリゲートと呼ぶ
(引数リスト)=>{}
P戻り値がvoid型の場合

(引数リスト)=>定数ないしダミー値
P戻り値がvoid型以外で定数ないしダミー値を返す場合

●実装例/サンプルコード

●関連するパターン

  • オブジェクト指向プログラミングのデザインパターンにおけるヌル(ナル)オブジェクト

■判断条件式

●目的

  • 条件判断の根拠をオブジェクトなどに含める際、値ではなく判定式を含めることで、仕様変更の影響範囲を広げない

●動機

  • 条件判断を行う根拠となる値をオブジェクトなどに含めることはよく行われるが、式そのものは固定されているので、式の変更が必要とされる修正要求への対応の手間が過大になりすぎる。この問題を解決するため、根拠となる値ではなく判定を行う式そのものをオブジェクトなどに含める

●適用可能性

  • 条件判断の根拠を対象となるオブジェクトなどに依存している

●構造

  • 条件判断を行う式をPredicate<……>型やFunc<……,bool>型のデリゲート値としてラムダ式などで記述し、オブジェクトなどに格納しておく。これを利用して、条件判断を行う
  • 結果が決まっている場合は「()=>true」や「()=>false」のようなnullデリゲートを使用する

●実装例/サンプルコード

●関連するパターン

  • nullデリゲート

■コンパイル時にチェック可能な名前付きコレクション

●目的

  • 名前の付いた価や参照の集まりを作成するが、存在するアイテムの名前を参照していることをコンパイル時にチェックできる

●動機

  • 通常、名前と値のペアを扱う場合、Dictionaryクラスなどを使うが、たとえ使用される名前がすべてコンパイル時に確定している場合でも、名前の正しさをコンパイル時にチェックできない。クラスを単なる名前と値の入れ物として使うことで、コンパイル時の名前のチェックを可能とする

●適用可能性

  • 名前の値のペアを扱う
  • コンパイル時に静的にすべての名前が確定している
  • 使用する名前がC#の要求する名前の規則に合致するか、あるいは合致する名前に変形して使用することが許される

●構造

  • 静的なクラスにフィールドとして名前を書き込み、それに値を入れて使用する
  • 名前や値の列挙は、リフレクションを用いて行う
  • 実際に格納される型と利用する際の型に相違がある場合、その相違を吸収するアクセサをプロパティなどで記述してもよい
  • 実行ファイルに難読化を施しても元の名前をプログラムから参照したい場合は、このクラスを難読化の対象からはずすか、属性として名前をフィールドに付加して、それを参照することができる
  • 格納するペアが多い場合は、部分クラスを用いて分割してよい
  • リフレクションは遅い処理なので、列挙処理が多発する場合は、リフレクションで列挙した結果を保存しておき、2回目以降はそれを使う

●実装例/サンプルコード

●関連するパターン

(特になし)

■汎用雑居クラス

●目的

  • どのクラスにも属さないで有意義な機能を提供するメソッドなどを入れておく便宜上の入れ物を提供する

●動機

  • 動作のカスタマイズの主役がクラスからメソッドに変わると、メソッド単体で有意義な機能を発揮するものも珍しくなくなる。しかし、C#ではクラスや構造体などに属さないメソッドは記述できない。そこで、便宜上の入れ物が必要とされる

●適用可能性

  • どのクラスなどにも属する必然性が存在しないメソッドなどがある

●構造

  • Generalなどの汎用性をイメージさせる名前の静的なクラスを作り、そこに固有の居場所を持たないメソッドなどを集める

●実装例/サンプルコード

●関連するパターン

(特になし)

■getter/setterによる参照

●目的

  • 参照渡しできない対象に対して、参照渡しと同等の機能を実現する

●動機

  • プロパティは参照渡しできないが、参照渡しと同様の機能を使用したい場合がある

●適用可能性

  • メソッドに、引数によって読み書きされる何らかの対象を渡す必要がある
  • メソッド内で、その対象に対する読み書きが発生する
  • 読み書きする型と実際に格納される型の不一致があってもよい(getter/setterを記述するラムダ式などの内部で変換できる)

●構造

  • 対象から値を読み出すデリゲートと対象に値を書き込むデリゲートをメソッドの引数に渡す

●実装例/サンプルコード

●関連するパターン

(特になし)

■初期化テンプレートオブジェクト

●目的

  • オブジェクト初期化子を用いてreadonlyのフィールドを初期化する

●動機

  • オブジェクト初期化子は便利な構文だが、オブジェクトに対して外部から作用する機能であるため、readonlyのフィールドに書き込むことができない。この制約を回避するために、クッションとなる中間オブジェクトを挿入する

●適用可能性

  • readonlyのフィールドを持つクラスなどを対象とする
  • オブジェクト初期化子を用いて初期化を行う

●構造

  • readonlyフィールドを持つ対象クラスがあるとする。この中で初期化したいフィールドと同じフィールドをreadonly指定なしで含むテンプレートクラスを作成する
  • テンプレートクラスから対象クラスへの暗黙変換(Implicit Operator)を用意する
  • この状態で次のように記述する
対象クラス 名前 = new テンプレートクラス()
{
    オブジェクト初期化子の初期化リスト
};
* 構造の詳細は第13章「13.12 オブジェクト初期化子の使用例」を参照。

●実装例/サンプルコード

●関連するパターン

(特になし)

■フラットオブジェクト

●目的

  • object型(System.Objectクラス)を除き、他のクラスに対してスーパークラスにもサブクラスにもならないクラスを用いる

●動機

  • 振る舞いを自由にカスタマイズできるオブジェクトはしばしば必要とされる。しかし、継承によって定義を階層化することは、定義の修正が玉突き状に発生して膨大な手間になる可能性があるので避けたい

●適用可能性

  • 「フラットオブジェクトファクトリ」などのパターンを用いて、継承を使用せずとも目的の機能を達成できる
  • 別のやり方が、継承よりもコンパクトかつシンプルである
  • インターフェースの実装は、必要とされる場合は行う
  • 抽象(abstract)、仮想(virtual)を含まないクラスであり、単にメンバーを追加するだけの継承は許してもよい

●構造

(特になし)

●実装例/サンプルコード

●関連するパターン

  • フラットオブジェクトファクトリ

■フラットオブジェクトファクトリ

●目的

  • 複数のデリゲート型のメンバーを持つオブジェクトを作成し、初期化するメソッドを用意することを通して、そのオブジェクトがプライベートに参照する変数をWクラスにフィールドを追加することなく”提供する

●動機

  • 振る舞いを自由にカスタマイズできるオブジェクトはしばしば必要とされる。しかし、継承によって定義を階層化することは、定義の修正が玉突き状に発生して膨大な手間になる可能性があるので避けたい

●適用可能性

  • 2つ以上のデリゲート型メンバーを持つクラスなど
  • デリゲート型メンバーに代入されるラムダ式間でのみ共有される変数があると、特に有効性が高い

●構造

  • フラットオブジェクトの作成を行うメソッド(ファクトリメソッド)を用意し、そのメソッド内のローカル変数をラムダ式にキャプチャさせることで、同じオブジェクトに属する複数のデリゲート型メンバーに代入されるラムダ式間で、同一の変数を共有させる
  • そのような変数がない場合でも、共通のラムダ式を設定する場合は、ファクトリメソッドを作成するメリットがある
  • 1種類のオブジェクトを作成する複数のファクトリメソッドがあってよく、その場合は、ラムダ式の内容やキャプチャさせる変数のバリエーションが異なってもよい

●実装例/サンプルコード

using System;

// フラット オブジェクト
//(振る舞いがカスタマイズされるが
//  継承の対象にならず、定義は階層化されない)
public class 探偵
{
  public Action 証拠を与える;
  public Action 現状を報告;
}

// フラット オブジェクト ファクトリ クラス
public static class 探偵ファクトリ
{
  // ファクトリ メソッド
  public static 探偵 Create優秀探偵()
  {
    // 優秀探偵は証拠がありさえすれば犯人がわかる
    bool 証拠あり = false;

    探偵 優秀探偵 = new 探偵();
    優秀探偵.証拠を与える = () => { 証拠あり = true; };
    優秀探偵.現状を報告 =
      () => Console.WriteLine(証拠あり ? "証明終了です"
                            : "推理中です");
    return 優秀探偵;
  }

  // もう1つ別のファクトリ メソッド
  public static 探偵 Create通常探偵()
  {
    // 通常探偵は証拠の数を数えないと犯人がわからない
    int 証拠数 = 0;

    探偵 通常探偵 = new 探偵();
    通常探偵.証拠を与える = () => 証拠数++;
    通常探偵.現状を報告 = () => Console.WriteLine(証拠数 >= 3 ?
                  "犯人はおまえだ!" : "推理中です");
    return 通常探偵;
  }
}

class Program
{
  static void Main(string[] args)
  {
    探偵[] 探偵たち =
    {
      探偵ファクトリ.Create通常探偵(),
      探偵ファクトリ.Create優秀探偵(),
    };
    foreach (var d in 探偵たち)
    {
      d.証拠を与える();
      d.現状を報告();
    }
    // 出力
    // 推理中です
    // 証明終了です
  }
}

●関連するパターン

  • フラットオブジェクト
  • カスタマイズ可能なメソッド

 INDEX
  [完全版]究極のC#プログラミング
  ラムダ式を使用した事例/デザインパターン・ミニカタログ
    1.[補遺]ラムダ式を使用した事例
  2.C# 3.0 デザインパターン・ミニカタログ
 
インデックス・ページヘ  「[完全版]究極のC#プログラミング」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間