2013年3月23日土曜日

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

.net Framework 1.0のころに書いた、こんなコードがありました。名前空間付きのXML文書をXPathに処理させるため、XmlNamespaceManagerを構築する前処理で、XML文書の指定ノードから、XML名前空間のプレフィックスと名前空間名を辞書の形で取り出すメソッドです。
public static StringDictionary 
    GetNodeNamespaces(XmlNode node, string defaultNSSubstitute)
{
  // 名前空間用辞書オブジェクトの生成
  StringDictionary namespaces = new StringDictionary();

  // 対象XMLノードからXML属性コレクションを取得
  XmlAttributeCollection attribs = node.Attributes;

  // XML属性すべてについて
  foreach (XmlAttribute attrib in attribs)
  {
    // "xmlns"で始まる場合
    if (attrib.Name.StartsWith("xmlns"))
    {
      string prefix = null;

      // デフォルトの名前空間の場合
      if (attrib.Name == "xmlns")
      {
        // 代替が指定されていれば代替文字列をプリフィクスにして登録
        if (!string.IsNullOrEmpty(defaultNSSubstitute))
          prefix = defaultNSSubstitute;
        else
          prefix = string.Empty;
      }

      // デフォルト以外の場合
      else if (attrib.Name[5] == ':' && attrib.Name.Length > 6)
        prefix = attrib.Name.Substring(6);

      // 名前空間用辞書にプリフィクスと名前空間のペアを追加
      if (prefix != null)
      {
        namespaces.Add(prefix, attrib.Value);
      }
    }
  }

  return namespaces;
}

昔書いたこのコードを使いまわそうとして、多少書き直しをしておこうと思い、この辺を直してみようと思いました。
  • StringDictionaryをDictinaory<string, string>に。
  • varを使う。
  • foreach+ifのブロックを、Whereを使ってネストを一つ減らす。
DOMのコレクション類は、IEnumerable<T>の実装ではないですが、IEnumerableは実装しているので、拡張メソッドCast<T>()を使えばWhere拡張メソッドも使えます。
public static Dictionary<string, string>
    GetNodeNamespaces(XmlNode node, string defaultNSSubstitute)
{
  var namespaces = new Dictionary<string string>();

  foreach (var attrib in node.Attributes.Cast<XmlAttribute>()
                             .Where(a => a.Name.StartsWith("xmlns")))
  {
    string prefix = null;

    if (attrib.Name == "xmlns")
    {
      if (!string.IsNullOrEmpty(defaultNSSubstitute))
        prefix = defaultNSSubstitute;
      else
        prefix = string.Empty;
    }

    else if (attrib.Name[5] == ':' && attrib.Name.Length > 6)
      prefix = attrib.Name.Substring(6);

    if (prefix != null)
    {
      namespaces.Add(prefix, attrib.Value);
    }
  }

  return namespaces;
}

foreachループの中で、デフォルト名前空間のプレフィックスを決定していますが、よく考えたらforeachの外でも処理できます。さらに、せっかくなのでNULL合体演算子を使いましょう。
public static Dictionary<string, string>
    GetNodeNamespaces(XmlNode node, string defaultNSSubstitute)
{
  var defprefix = defaultNSSubstitute ?? string.Empty;
  var namespaces = new Dictionary<string, string>();

  foreach (var attrib in node.Attributes.Cast<XmlAttribute>()
                             .Where(a => a.Name.StartsWith("xmlns")))
  {
    string prefix = null;

    if (attrib.Name == "xmlns")
      prefix = defprefix;
    else if (attrib.Name[5] == ':' && attrib.Name.Length > 6)
      prefix = attrib.Name.Substring(6);

    if (prefix != null)
    {
      namespaces.Add(prefix, attrib.Value);
    }
  }

  return namespaces;
}

んー。なんかあと一歩な感じがする。もうちょっと考えてみると、foreachループの中で、辞書に突っ込むケースと突っ込まないケースがある。これをそもそもWhereで辞書に突っ込まないケースはフィルタしてしまえばよいかもしれない。

辞書に突っ込むケースというのは、XML属性名が「xmlns」か、「xmlns:」で始まる場合のいずれかのみ。
public static Dictionary<string, string>
    GetNodeNamespaces(XmlNode node, string defaultNSSubstitute)
{
  var defprefix = defaultNSSubstitute ?? string.Empty;
  var namespaces = new Dictionary<string, string>();

  foreach (var attrib in node.Attributes.Cast<XmlAttribute>()
           .Where(a => a.Name == "xmlns" || a.Name.StartsWith("xmlns:")))
  {
    string prefix = null;

    if (attrib.Name == "xmlns")
      prefix = defprefix;
    else
      prefix = attrib.Name.Substring(6);

    namespaces.Add(prefix, attrib.Value);
  }

  return namespaces;
}

これなら三項演算子を使って、変数をインライン化してしまえば、
public static Dictionary<string, string>
    GetNodeNamespaces(XmlNode node, string defaultNSSubstitute)
{
  var defprefix = defaultNSSubstitute ?? string.Empty;
  var namespaces = new Dictionary<string, string>();

  foreach (var attrib in node.Attributes.Cast<XmlAttribute>()
           .Where(a => a.Name == "xmlns" || a.Name.StartsWith("xmlns:")))
  {
    namespaces.Add(attrib.Name == "xmlns" ? 
                   defprefix : attrib.Name.Substring(6), attrib.Value);
  }

  return namespaces;
}
こうなると、IEnumerable<T>をDictionaryに変換しているだけなので、
public static Dictionary<string string>
    GetNodeNamespaces(XmlNode node, string defaultNSSubstitute)
{
  var defprefix = defaultNSSubstitute ?? string.Empty;

  return node.Attributes.Cast<xmlattribute>()
      .Where(a => a.Name == "xmlns" || a.Name.StartsWith("xmlns:"))
      .ToDictionary(a => a.Name == "xmlns" ?
                    defprefix : a.Name.Substring(6), a => a.Value);
}

これで済んじゃうなぁ…。最初のコードとの違いに我ながら笑えました。

0 件のコメント:

コメントを投稿