連載
.NETで始めるデザインパターン

第3回 リファクタリングにより導き出すStrategyパターン

太陽システム株式会社 中西 庸文
2005/03/19
Page1 Page2 Page3

■リファクタリング1:Strategy階層のトップに位置するクラスの作成

 最初に「価値」を出力するためのアルゴリズムをクラスに分離して独立させよう。具体的にはTextFormatterクラスを作成する。このクラスは、出力のためのクラスの階層(Strategy階層)のトップに位置することになるクラスである。

using System;

namespace DesignPatterns.Core.TemplateMethod
{
  public class TextFormatter
  {
  }
}
新しく作成したTextFormatterクラス(C#)

 このコードをコンパイルして冒頭で作成したテストを実行する。もちろんコンパイルとテストは正常に完了するはずだ。

■リファクタリング2:「メソッドの移動(F)」によるアルゴリズムの集合の分離

 「価値」を出力するためのTextFormatterクラスが存在する状態では、「価値」を出力するという実行責任(アルゴリズム)が、XpValuesクラスに割り当てられたままになっているのは不適切だ。

 そこで「メソッドの移動(F)」を行い、この責任を適切にTextFormatterクラスに割り当て直す。

 最初の準備として、XpValuesクラスのFormatメソッドとその内部で呼び出されているGetTitleメソッドをTextFormatterクラスへと移動しよう。

using System;
using System.Text;

namespace DesignPatterns.Core.TemplateMethod
{
  public class TextFormatter
  {
    // XpValuesクラスからコピー
    public string Format()
    {
      if (formatType == 0)
      {
        StringBuilder builder = new StringBuilder();
        builder.Append(GetTitle() + "\r\n");
        foreach (string xpValue in values)
          builder.Append("・" + xpValue + "\r\n");
        return builder.ToString();
      }
      else
      {
        StringBuilder builder = new StringBuilder();
        builder.Append("<p>" + GetTitle() + "</p>\r\n");
        builder.Append("<ul>\r\n");
        foreach (string xpValue in values)
          builder.Append("<li>" + xpValue + "\r\n");
        builder.Append("</ul>\r\n");
        return builder.ToString();
      }
    }

    // XpValuesクラスから移動
    private string GetTitle()
    {
      return name + "の" + values.Count.ToString() + "つの価値";
    }
  }
}
XpValuesクラスから移動されたTextFormatterクラスのFormatメソッドとGetTitleメソッド(C#)

 TextFormatterクラスへと移動した上記のFormatメソッドとGetTitleメソッドの内部では、移動元であるXpValuesクラスのフィールドを参照している個所があったため、このままではコンパイルが通らない。

 そこでFormatメソッドとGetTitleメソッドにはXpValuesクラスのインスタンス自体をパラメータとして引き渡すことにしよう。つまりXpValuesクラスのフィールドの参照はすべてXpValuesクラスのプロパティの参照へと置き換えるわけだ。

 またGetTitleメソッドは、このままのメソッド名ではTextFormatterのタイトルを取得するメソッドだと勘違いしてしまうので、「メソッド名の変更(F)」を行い、GetTitleメソッドの名前をより適切な名前であるGetTitleForメソッドへと変更する。

 具体的には次のようなコードとなる。

using System;
using System.Text;

namespace DesignPatterns.Core.TemplateMethod
{
  public class TextFormatter
  {
    // 移動元のオブジェクト参照をパラメータにする
    public string Format(XpValues xpValues)
    {
      if (xpValues.FormatType == 0)
      {
        StringBuilder builder = new StringBuilder();
        builder.Append(GetTitleFor(xpValues) + "\r\n");
        foreach (string xpValue in xpValues)
          builder.Append("・" + xpValue + "\r\n");
        return builder.ToString();
      }
      else
      {
        StringBuilder builder = new StringBuilder();
        builder.Append("<p>" + GetTitleFor(xpValues) + "</p>\r\n");
        builder.Append("<ul>\r\n");
        foreach (string xpValue in xpValues)
          builder.Append("<li>" + xpValue + "\r\n");
        builder.Append("</ul>\r\n");
        return builder.ToString();
      }
    }

    // メソッド名の変更
    private string GetTitleFor(XpValues xpValues)
    {
      return xpValues.Name + "の" + xpValues.Count.ToString() + "つの価値";
    }
  }
}
XpValuesクラスのフィールドの参照がプロパティの参照へ置き換えられたTextFormatterクラスのFormatメソッドとGetTitleメソッド(C#)

 このTextFormatterクラスのFormatメソッドでは、XpValuesクラスのFormatTypeプロパティの値によって条件分岐を行うように変更したが、XpValuesクラスにはFormatTypeプロパティが存在しないのでこれを追加する。

 次に、中身をTextFormatterクラスへ移動したXpValuesクラスのFormatメソッドでは、TextFormatterクラスのインスタンスを生成して、そのFormatメソッドを呼び出すように変更する。

 これらを行ったのが次のコードだ。

using System;
using System.Collections;
using System.Text;

namespace DesignPatterns.Core.TemplateMethod
{
  public class XpValues : IEnumerable
  {
    ……中略……

    // プロパティを追加
    public int FormatType
    {
      get { return formatType; }
    }
    ……中略……

    public string Format()
    {
      // 生成したTextFormatterオブジェクトの
// Formatメソッドを呼び出す
      return new TextFormatter().Format(this);
    }
  }
}
XpValuesクラスにおける移動元Formatメソッドの変更とFormatTypeプロパティの追加(C#)

 ここまでに作成したコードをコンパイルして、再びテストを実行しよう。今度は正常にコンパイルが完了し、テストも正常にパスするはずだ。

 これで「メソッドの移動(F)」が完了した。

 その結果として「価値」を出力するためのアルゴリズムの集合がXpValuesクラスから分離され、Strategy階層のトップとなるTextFormatterクラスへ移動された。しかしTextFormatterクラスのFormatメソッドには、条件記述や重複したアルゴリズムなどの問題が依然として残されたままになっている。従ってさらにリファクタリングが必要だ。

 以降のリファクタリングの計画を次に示す。

 TextFormatterクラスのFormatメソッド内には、XpValuesクラスのFormatTypeプロパティの値によって異なる振る舞いをするアルゴリズムの集合が存在する。そこで各アルゴリズムをカプセル化して交換可能にするために、TextFormatterクラスの派生クラスを作成してFormatメソッドをオーバーライドさせる。

 つまりStrategy階層を構築して各Strategyに異なるアルゴリズムを持たせ、それらを切り替えることで異なる振る舞いを実現させる。このようにして条件記述を書かなくてもいいようにしてしまうのだ。

■リファクタリング3:「Extract Parameter(K)」(パラメータの抽出)による各Strategyのインスタンス切り替えの準備

 XpValuesクラスのFormatメソッドで行っているTextFormatterクラスのインスタンス生成(上記コードの「new TextFormatter()」の部分)の記述がハードコーディングされたままだと、これから作成することになる各Strategy(TextFormatterクラスの派生クラス)のインスタンスを切り替えることができなくなる。

 そこで、ハードコーディングされているTextFormatterクラスのインスタンス生成の記述をやめ、あらかじめ生成したインスタンスをフィールド(下記コードの「private TextFormatter formatter」の部分)により保持するようにする。実際のインスタンス化はプライベート・コンストラクタ内で行う。

using System;
using System.Collections;

namespace DesignPatterns.Core.TemplateMethod
{
  public class XpValues : IEnumerable
  {
    ……中略……

    // フィールドを追加
    private TextFormatter formatter;

    private XpValues(string name, int formatType)
    {
      this.name = name;
      this.formatType = formatType;
      // インスタンス生成
      this.formatter = new TextFormatter();
    }
    ……中略……

    public string Format()
    {
      // インスタンス生成をやめ、フィールド参照へ変更
      return formatter.Format(this);
    }
  }
}
XpValuesクラスにおけるTextFormatter型フィールドの追加(C#)

 またコンパイルしてテストを実行しよう。正常にテストが通るはずだ。

 各Strategyのインスタンスを保持するフィールドが作成されたので、次はインスタンスを切り替える方法を考えよう。今回は、あらかじめ外部で生成された各StrategyのインスタンスをXpValuesクラスのプライベート・コンストラクタに引き渡す方法を採用する。

 そこで「Extract Parameter(K)」(パラメータの抽出)を行い、XpValuesクラスのプライベート・コンストラクタで生成されているTextFormatterクラスのインスタンスを、コンストラクタのパラメータとして抽出する。

 また、TextFormatterクラスのインスタンス生成は、CreatePlainTextFormatXpValuesメソッドとCreateHtmlTextFormatXpValuesメソッドで行うことにする。

 具体的なコードは次のとおりだ。

using System;
using System.Collections;

namespace DesignPatterns.Core.TemplateMethod
{
  public class XpValues : IEnumerable
  {
    ……中略……

    // パラメータの抽出が行われたプライベート・コンストラクタ
    private XpValues(string name, int formatType, TextFormatter formatter)
    {
      this.name = name;
      this.formatType = formatType;
      // インスタンス生成をパラメータに置き換え
      this.formatter = formatter;
    }

    public static XpValues CreatePlainTextFormatableXpValues(string name)
    {
      // 生成したインスタンスをパラメータとして引き渡す
      return new XpValues(name, 0, new TextFormatter());
    }

    public static XpValues CreateHtmlTextFormatableXpValues(string name)
    {
      // 生成したインスタンスをパラメータとして引き渡す
      return new XpValues(name, 1, new TextFormatter());
    }
    ……中略……
  }
}
プライベート・コンストラクタにTextFormatter型のパラメータが追加されたXpValuesクラス(C#)

 再度、コードをコンパイルしてテストを実行してみよう。これも正常に実行できるはずだ。

 これで「Extract Parameter(K)」が完了した。

 その結果として、各Strategyのインスタンス切り替えの準備が整った。以降のリファクタリングで、CreatePlainTextFormatableXpValuesメソッドではプレーン・テキスト形式で価値の出力を行うStrategyが生成され、CreateHtmlTextFormatableXpValuesメソッドではHTML形式で価値の出力を行うStrategyが生成されることになる。

 それでは次に、条件記述に関する問題を解決するための最後のリファクタリングを行うことにしよう。


 INDEX
  .NETで始めるデザインパターン
  第3回 リファクタリングにより導き出すStrategyパターン
    1.リファクタリング前の設計
  2.リファクタリングによるStrategyパターンの適用
    3.ポリモーフィズムによる条件記述の置き換え
 
インデックス・ページヘ  「.NETで始めるデザインパターン」


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 記事ランキング

本日 月間