2013年3月10日日曜日

ラムダ式を利用したリファクタリングの例


ラムダ式とは?


ラムダ式とは、.net Framework 3.5(Visual Studio 2008)以降で採用された、「デリゲートを簡潔に書くための構文」だと思ってもらえればよいと思います。

デリゲートとは…。C/C++をかじったことのある人なら、「コールバック関数」や「関数ポインタ」にあたります。VBならイベントを通知する仕組みが近いです。かじってない人は…。関数(処理)をオブジェクトにして、オブジェクト化された関数を別のところで呼び出す仕組み、とでも言えばいいでしょうか…。

馴染みがないと非常にわかりにくいと思いますので、サンプルを2つほど挙げてみます。

ラムダ式を使ったリファクタリングのサンプル (1)


設計


まずは設計から。

  • クラスXxxAccessorは、ストレージに対してInsert/Delete/Updateが可能。
  • Insert/Delete/Updateの各処理は、すべて以下の順序で行う。
    • プロキシオブジェクトの生成
    • ログ出力(親クラスのメソッドを使用)
    • プロキシオブジェクト経由でのInsert/Delete/Update
    • プロキシオブジェクトの破棄
    • 例外の発生有無にかかわらず、クリーンナップ(親クラスのメソッドを使用)
  • Insert/Delete/Updateの各処理は、例外発生時はその例外をそのまま外にスローする。

実装


上記の設計に対して、以下のように実装しました。

public class NoLambdaAccessor : Accessor
{
  public void Insert(string line)
  {
    try
    {
      using (AbstractProxy proxy = AbstractProxy.Create())
      {
        Log("start");
        proxy.Insert(line);
      }
    }
    finally
    {
      Cleanup();
    }
  }

  public void Delete(int id)
  {
    try
    {
      using (AbstractProxy proxy = AbstractProxy.Create())
      {
        Log("start");
        proxy.Delete(id);
      }
    }
    finally
    {
      Cleanup();
    }
  }

  public void Update(int id, string line)
  {
    try
    {
      using (AbstractProxy proxy = AbstractProxy.Create())
      {
        Log("start");
        proxy.Update(id, line);
      }
    }
    finally
    {
      Cleanup();
    }
  }
}

見ての通り、3つのメソッドはほぼ同じ処理で、プロキシオブジェクト経由の呼び出し処理の行のみが異なっています。

追加の設計


上記コードに対して、今度はInsert(string line, int number)と、Update(int id, string line, int number)の2つのオーバーロードを追加する必要が生じました。

現状のまま2つのメソッドを追加してもよいのですが、リファクタリングを実施して冗長な部分を除いてから追加したいと思いました。

さて、どうリファクタリングしましょうか?

リファクタリング


リファクタリングの仕方はいろいろあると思いますが、ラムダ式を使って以下の様にリファクタリングしてみました。

public class LambdaAccessor : Accessor
{
  private void Access(Action<AbstractProxy> action)
  {
    try
    {
      using (AbstractProxy proxy = AbstractProxy.Create())
      {
        Log("start");
        action(proxy);
      }
    }
    finally
    {
      Cleanup();
    }
  }

  public void Insert(string line)
  {
    Access(proxy => proxy.Insert(line));
  }

  public void Delete(int id)
  {
    Access(proxy => proxy.Delete(id));
  }

  public void Update(int id, string line)
  {
    Access(proxy => proxy.Update(id, line));
  }
}

戦略としては、
  • 3つのメソッドに共通するコードのうち、異なる部分をデリゲートにしたプライベートメソッド「Access」を用意する。
  • 3つのメソッドは、個別の処理をラムダ式で表現したデリゲートを用意し、そのデリゲートを引数として上記Accessメソッドを呼び出す。

これならぱっとソースコードを眺めてみたとき、それほど違和感はないんじゃないかと思いますが、どうでしょう?

また、このようにリファクタリングしておくことで、2つのオーバーロードメソッドを追加するにしても、中身が1行のメソッドを2つ追加すれば対応完了となります。(もちろん、対応するユニットテストコードは書く必要がありますが。)

なお、Accessメソッドの引数「Action<Xxx>」は、.net Frameworkで定義済みのデリゲートです。以下の様に定義されています。

namespace System { public delegate void Action<T>(T obj); }

ラムダ式を使ったリファクタリングのサンプル (2)


設計


こちらも設計から。

  • クラスInputCheckerは、複数のコントロールの入力チェックを行う。
  • コントロールを示すクラスControlは、Tagプロパティ、Textプロパティ、Validプロパティを持ち、それぞれのプロパティは以下の通りの意味を持つ。
    • Textプロパティはユーザの入力文字列を保持する。
    • Tagプロパティは「require」、「number」、それ以外のいずれかの文字列で、「require」の場合入力は必須。「number」の場合数字のみ入力可。それ以外の場合は入力制限は特になし。
    • Validプロパティは入力チェックの結果NGの場合falseとなる。(この時コントロールの背景はピンク色で表示される。)
  • InputCheckメソッドはコントロールの配列を受け取り、以下の処理を行う。
    • コントロール配列のすべてについて、以下の処理を行う。
      • コントロールのTagがrequireで、Textが空の場合、Validをfalseにする。
      • すべてのコントロールのうち、一つでもValidがfalseのものがある場合、適切なエラーメッセージを表示して処理を返す。
    • 再びコントロール配列のすべてについて、以下の処理を行う。
      • コントロールのTagがnumberで、Textが数字以外が含まれている場合、Validをfalseにする。
      • すべてのコントロールのうち、一つでもValidがfalseのものがある場合、適切なエラーメッセージを表示して処理を返す。

実装


ちょっと面倒な設計ですが、以下のように実装しました。

public class NoLambdaInputChecker : InputChecker
{
  public void InputCheck(List<Control> controls)
  {
    bool allvalid = true;
    int result;

    foreach (Control c in controls)
    {
      if (c.Tag == "require")
      {
        if (string.IsNullOrEmpty(c.Text))
        {
          allvalid = false;
          c.Valid = false;
        }
      }
    }

    if (!allvalid)
    {
      Msg("require field is not set");
      return;
    }

    foreach (Control c in controls)
    {
      if (c.Tag == "number")
      {
        if (!int.TryParse(c.Text, out result))
        {
          allvalid = false;
          c.Valid = false;
        }
      }
    }

    if (!allvalid)
    {
      Msg("not a number");
    }
  }
}

見ての通り、似たような処理が縦に2つ並んでますね。

追加の設計


上記コードに対して、今度は「Tagプロパティがdateだった時に、Textプロパティは数字のみ8ケタでない、または、yyyyMMdd形式で日付として正しくない場合はValidプロパティをfalseにする」という仕様追加を行う必要が生じました。

現状のままInputCheckメソッドを修正してもよいのですが、リファクタリングを実施して冗長な部分を除いてから追加したいと思いました。

さて、どうリファクタリングしましょうか?

リファクタリング


これもリファクタリングの仕方はいろいろあると思いますが、ラムダ式を使って以下の様にリファクタリングしてみました。

public class LambdaInputChecker : InputChecker
{
  private bool InputCheckByCondition
        (List<Control> controls, Func<Control, bool> condition)
  {
    bool allvalid = true;
    foreach (Control c in controls.Where(c => condition(c)))
    {
      allvalid = false;
      c.Valid = false;
    }
    return allvalid;
  }

  public void InputCheck(List<Control> controls)
  {
    int result;

    if (InputCheckByCondition(controls, c =>
        c.Tag == "require" && string.IsNullOrEmpty(c.Text)))
    {
      Msg("require field is not set");
      return;
    }

    if (InputCheckByCondition(controls, c =>
        c.Tag == "number" && !int.TryParse(c.Text, out result)))
    {
      Msg("not a number");
      return;
    }
  }
}

戦略としては、
  • 縦に並んでいる似たようなコードのうち、条件判断の部分のみをデリゲートにしたプライベートメソッド「InputCheckByCondition」を用意する。
  • 縦に並んだ2つの似たような処理は、個別の条件判断をラムダ式で表現したデリゲートを用意し、そのデリゲートを引数として上記InputCheckByConditionメソッドを呼び出す。

前のサンプルよりは複雑ですが、元のコードと比べてこっちのほうがぱっと見わかりやすいんじゃないかと思いますが、どうでしょう?

そして、このようにリファクタリングしておくことで、追加の仕様変更に対しては、条件判断と表示メッセージのみを変更した一つのifブロックを追加すればOKとなります。

なお、InputCheckByConditionメソッドの引数「Func<Xxx, Yyy>」は、.net Frameworkで定義済みのデリゲートです。以下の様に定義されています。

namespace System { public delegate TResult Func<T, TResult>(T arg); }

まとめ


上記で上げた2つのサンプルのようなケースでは、最初からラムダ式を検討する必要はないと思いますが、リファクタリングの際に威力を発揮します。

また、使ってみればわかりますが、Visual StudioのIntelliSenseが大変賢く、ラムダ式の中でもちゃんと効いてくれるため、楽に使うことができます。

機能追加の際などに、コードを見て冗長に見えて、リファクタリングを考えてみてもうまくいきそうにないとき、「ラムダ式で、一部の処理や条件判断を置き換えられるようにしたらどうか?」と考えてみると、うまくリファクタリングできるケースが多いです。

0 件のコメント:

コメントを投稿