2013年3月26日火曜日

今週のヤンマガ

『センゴク一統記』。いよいよ本能寺も佳境。面白いんだけど、光秀が謀反に至った経緯が今一つピンとこなかった。まぁ、いろいろ小説とかも読んだけど、ピンと来たことなんてないから、イインダケドネ。

それよりも、ここからのイベント目白押しが楽しみ。

高松城攻めを講和→清水宗治切腹→中国大返し→山崎の戦い→賤ヶ岳の戦い→小牧・長久手の戦い→四国攻め→九州攻め。

ここまでいってようやっとゴンベイが失脚するわけだよ。あと何年連載するつもりなのかしらん。


…それと、そういえば『ハチイチ』は無かったか。残念。

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);
}

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

2013年3月22日金曜日

忘れたころに『ドリフターズ』3巻

『ドリフターズ』(平野耕太)の3巻が発売されてました。もう一回1~2巻読まないとついてけないぞ。

確か2巻の終わりでは、世界中のいろんな時代で夭折した有名人がわらわらと異世界にやってきたあたりで終わっていたような記憶がある。あってる?

マンガ大賞2013

マンガ大賞2013が決まったそうです。今年は吉田秋生さんの『海街dialy』。おめでとーございまーす!!

とはいえ、今年のノミネート作品。ほとんど読んだことがない。読んでるのは『山賊ダイアリー』くらい。大賞作品位は機会を見つけて読んでみたいな。

2013年3月18日月曜日

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

こんな要求があったとします。

  • Shift-JISのCSVファイルを入力し、UTF-8のCSVファイルを出力する。
  • 入力したCSVの各カラムは、固定長で前後に空白が入る可能性があり、その空白は除去して出力する。
  • 各行の先頭のカラムはIDになっていて、特定のIDのみ出力対象とする。
この処理を実現するために、C#でこんなコードを書きました。(『特定のIDのみ出力』の処理は、単純化のため奇数のみ出力するようにしています。)

var sb = new StringBuilder();
string line;

using (var sr = new StreamReader(inFile, Encoding.GetEncoding("shift-jis")))
using (var sw = new StreamWriter(outFile))
{
  while ((line = sr.ReadLine()) != null)
  {
    var items = line.Split(',');
    sb.Length = 0;

    if (int.Parse(items[0]) % 2 != 0)
    {
      foreach (var item in items)
      {
        sb.Append(item.Trim()).Append(',');
      }
      sw.WriteLine(sb.ToString(0, sb.Length - 1));
    }
  }
}

まぁ、これで全く問題はないんですが、もうちょっと書きようはないか考えてみました。試行錯誤の結果、3つのstaticメソッドを作ることで、以下のような、割と好みのコードができました。

var sb = new StringBuilder();

using (var sr = new StreamReader(inFile, Encoding.GetEncoding("shift-jis")))
using (var sw = new StreamWriter(outFile))
{
  sw.WriteLines(sr.GetLineEnumerator()
    .Select(line => line.Split(','))
    .Where(items => int.Parse(items[0]) % 2 != 0)
    .Select(items => sb.Clear().AppendAll(
       items.Select(i => i.Trim() + ",")).ToString(0, sb.Length - 1)));
}

新たに作った3つのstaticメソッドは、以下のものすごくシンプルなものです。

1つめは、TextReaderクラスから各行ごとの読み込みを列挙可能なものにする拡張メソッド。
public static IEnumerable<string> GetLineEnumerator(this TextReader tr)
{
  string s;
  while ((s = tr.ReadLine()) != null)
  {
    yield return s;
  }
}

2つめは、TextWriterクラスに列挙可能な文字列配列等を渡して、各行ごとに出力する拡張メソッド。
public static void WriteLines(
          this TextWriter tw, IEnumerable<string> values)
{
  foreach (var line in values)
  {
    tw.WriteLine(line);
  }
}

3つめは、StringBuilderクラスに列挙可能な文字列配列等を渡して文字列を追記していく拡張メソッド。
public static StringBuilder AppendAll(
          this StringBuilder sb, IEnumerable<string> values)
{
  foreach (var item in values)
  {
    sb.Append(item);
  }
  return sb;
}

個人的にはこれで満足。

『でも結局ライン数もタイプ数も増えちゃってるじゃん。』…まったくもってその通りでございます。

でもでも、この3つのstaticメソッドを使いまわせるようにしておけば、かなり使い出のあるライブラリになるんすよ。

たとえば、テキストファイルを読み取って、文字列配列を作りたければこう書けるし、
using (var reader = new StreamReader(inFile))
{
  return reader.GetLineEnumerator().ToArray();
}

逆に文字列配列をテキストファイルにザーッと出力させたいときはこう書ける。
using (var writer = new StreamWriter(outFile))
{
  writer.WriteLines(lines);
}

あるいはたとえば、ネットワークストリームからの入力をファイルに落とすときはこう。
using (var reader = new StreamReader(netstream))
using (var writer = new StreamWriter(outFile))
{
  writer.WriteLines(reader.GetLineEnumerator());
}


こうやって、手持ちのライブラリを増やしていくと、よりコードを書くのが楽しく、ラクになると思うんす。

ちなみに、ここで一つ悩ましいのは、「TexReader.GetLineEnumerator」のメソッド名。「TextWriter.WriteLines」との対称性があったほうが分かりやすいんですが、「TextReader.ReadLines」としちゃうと、 その場でReadしてstring[]の戻りの型を予想しちゃうので、遅延実行のイメージがまるでなくなっちゃうため、あえて対称性を崩す形にしてます。が、これが正解かどうかはわからんですね。

もひとつちなみに、このコードはVS2008/.net 3.5だと失敗します。なんと、「StringBuilder.Clear」メソッドが.net 4.0以降にしかないのです。が、それならClearメソッドをStringBuilderクラスの拡張メソッドで作っちゃえば問題なしです。なんとかなります!


(2013/06/23 追記)
String.Joinを使えば、StringBuilderの拡張メソッドは不要でした。「SJISのCSVファイルを各カラムの操作と条件による絞り込みを行い、UTF-8のCSVファイルを出力する処理をLINQで。」で別記事で書きました。

(2015/10/22 追記)
FileクラスにstaticメソッドのReadLines/ReadAllLines/WriteAllLinesというテキスト行操作メソッド群があったんですね。今日の今日まで知りませんでした。はずかしいいいい。
そんなわけで最新の記事は「SJISのCSVファイルを各カラムの操作と条件による絞り込みを行い、UTF-8のCSVファイルを出力する処理をLINQで。(改)」に書き直しました。

2013年3月17日日曜日

今ハズセナイ連載中漫画 (2013年春版)


今楽しみにしている、連載中漫画リスト。さすがに全部掲載誌で読んでるわけじゃないけど。
  • 週刊少年マガジン
    • あひるの空/日向武史
    • ベイビーステップ/勝木光
    • アゲイン!!/久保ミツロウ
    • 七つの大罪/鈴木央
    • 波打際のむろみさん/名島啓二
    • 我妻さんは俺のヨメ/蔵石ユウ・西木田景志
  • 週刊少年サンデー
    • 神のみぞ知るセカイ/若木民喜
    • マギ/大高忍
    • 銀の匙 Silver Spoon/荒川弘
    • 絶対可憐チルドレン/椎名高志
    • ハヤテのごとく!/畑健二郎
    • 最後は?ストレート!!/寒川一之
  • ヤングマガジン
    • センゴク一統記/宮下英樹
    • 雪にツバサ/高橋しん
    • エイト/楠みちはる
    • 8♀1♂/咲香里
  • ビックコミックスピリッツ
    • アイアムアヒーロー/花沢健吾
    • とめはねっ!/河合克敏
    • くーねるまるた/高尾じんぐ
    • あさひなぐ/こざき亜衣
  • ビックコミックスペリオール
    • 人生画力対決/西原理恵子
  • モーニング
    • GIANT KILLING/ツジトモ・網本将也
    • 宇宙兄弟/小山宙哉
    • グラゼニ/森高夕次・アダチ ケイジ
    • ひらけ駒!/南Q太
    • 鬼灯の冷徹/江口夏美
    • ピアノの森/一色まこと
    • ライスショルダー/なかいま強
  • イブニング
    • 少女ファイト/日本橋ヨヲコ
    • いとしのムーコ/みずしな孝之
    • ADAMAS/皆川亮二
    • ジャポニカの歩き方/西山優里子
    • プロチチ/逢坂えみ子
    • もやしもん/石川雅之
    • Eから弾きな。/佐々木拓丸
  • アフタヌーン
    • ヒストリエ/岩明均
    • ヴィンランド・サガ/幸村誠
  • ゲッサン
    • MIX/あだち充
    • アオイホノオ/島本和彦
  • グランドジャンプ
    • 瞬きのソーニャ/弓月光
    • ぼくの体はツーアウト/よしたに
    • アントルメティエ/早川光・きたがわ翔
  • その他
    • こどものじかん/私屋カヲル
    • 3月のライオン/羽海野チカ
    • ましろのおと/羅川真里茂
    • 純潔のマリア/石川雅之
    • 白衣のカノジョ/日坂水柯
    • kiss×sis/ぢたま某
    • よつばと!/あずまきよひこ

2013年3月14日木曜日

URI中の指定キーに対応するクエリ文字列の値を取り出す処理をLINQで。UTもついでに。

以前書いた、こんなコードがありました。

string valueKey1 = null;

foreach (string query in uri.Query.Split('&')) // uriはUriオブジェクト
{
  string[] kv = query.Split('=');

  if (kv.Length < 2)
    continue;
  if (kv[0].StartsWith("?"))
    kv[0] = kv[0].Substring(1);

  if (kv[0] == "key1")
  {
    valueKey1 = kv[1];
    break;
  }
}

要は、URIのクエリ文字列から、指定のパラメータの値を取り出す処理です。これをもう少し今っぽいコードにして、ついでに汎用化(ライブラリ化)して、さらについでにユニットテストまでやってしまおうと目論みました。

VS Express 2012 を起動し、新規プロジェクトをソリューション付きで、クラスライブラリをターゲットに作成します。


ライブラリ化にあたっての方針はこんな感じ。

  • Uriクラスの拡張メソッドにする。
  • パラメータ名を指定して値を取り出すメソッドを実装。
  • このメソッドにはオプショナルで、Uriエスケープの有無を指定できるようにする。ただし、VS2008とかでもソースをそのまま使えるように、メソッドのオーバーロードで対応する。
  • Uriエスケープありの場合、Uriエスケープをほどいた値を返す。

こうしてみました。

public static class UriExtensions
{
  public static string FindQuery(this Uri uri, string key)
  {
    return FindQuery(uri, key, true);
  }

  public static string FindQuery(this Uri uri, string key, bool uriEscape)
  {
    throw new NotImplementedException();
  }
}

テストコードを書きます。「テスト」メニューを開いてみると、「新しいテスト」メニューがないのね…。んじゃ、「新しいプロジェクト」で単体テストプロジェクトを、ソリューションに追加するように作成。


デフォルトで作成されたテストクラスの名前を変更して、参照設定にテスト対象プロジェクトを追加。さらにUsingを指定してテスト準備OK。

作成されたテストコードには、初期状態でClassInitializeもTestInitializeも無いんだ…。テストコードがすっきりしてるのはいいけど、いざテストの初期化をしようとしたときに面倒だなぁ。VS2010までで慣れてるといろいろ違和感がある。まぁ、そのうちなれるかな。

気を取り直して、テストコードを書き始めます。この辺が網羅できればいいでしょ。

  • Uriクエリ文字列のパラメータ数が、0、1、2以上。
  • 指定したキーがヒットする、しない。
  • Uriエスケープされている、されていない。
  • Uriエスケープのオプション指定をする、しない。

書いてみたテストコードがこれです。
[TestClass]
public class UriExtensionsTest
{
  [TestMethod]
  public void FindParamValueTest()
  {
    Uri u1 = new Uri("http://unkkown/test.html");
    Assert.IsNull(u1.FindQuery("unknownkey"));

    Uri u2 = new Uri("http://unknown/test.html?key1=xxx&key2=yyy&key3=zzz");
    Assert.IsNull(u2.FindQuery("key4"));
    Assert.AreEqual("yyy", u2.FindQuery("key2"));

    string key = "%E3%82%AD%E3%83%BC"; // "キー"のURIエンコード(UTF-8)
    string value = "%E5%80%A4"; // "値"のURIエンコード(UTF-8)
    Uri u3 = new Uri("http://unknown/test.html?" + key + "=" + value);
    Assert.AreEqual("値", u3.FindQuery("キー"));
    Assert.AreEqual(value, u3.FindQuery(key, false));
  }
}

テストを実行すると失敗して、「NotImplementedException」がスローされたとのこと。予定通り。ここからUriExtensionsクラスを実装します。試行錯誤の結果、テストをパスして出来上がったコードがこれ。

public static class UriExtensions
{
  public static string FindQuery(this Uri uri, string key)
  {
    return FindQuery(uri, key, true);
  }

  public static string FindQuery(this Uri uri, string key, bool uriEscape)
  {
    string query = uri.Query;
    string keyName = uriEscape ? Uri.EscapeDataString(key) : key;
    Func<string, string> vconv = s => s;

    if (string.IsNullOrEmpty(query))
      return null;
    if (query.StartsWith("?"))
      query = query.Substring(1);
    if (uriEscape)
      vconv = s => Uri.UnescapeDataString(s);

    return query.Split('&').Where(q => q.StartsWith(keyName + "="))
       .Select(q => vconv(q.Substring(keyName.Length + 1)))
        .FirstOrDefault();
  }
}

こんな感じでしょうかね。うん。 あとはコードコメントを書いてリリースビルドすれば作業完了です。そこは省略。

2013年3月13日水曜日

今週のイブニング

ここ最近のイブニングの面白さったら!! アタリが多くて読むのに時間がかかる。

問題は、表紙に『CAPTAIN アリス』と書いているのに、普通に休載してること。単行本発売だからだろうけど、わかりづらいよ。

あるフォルダ以下の全ファイルサイズの合計を求める処理をLINQで。

サンプルとして、あるフォルダ以下のすべてのファイルの、合計サイズを算出するアプリケーションを作ります。

VS2012 Express for Desktopを使って、コンソールアプリケーションを作り、第一引数に総サイズを取得したいフォルダを指定するものとします。

フォルダ以下のフォルダの列挙、ファイルの列挙はSystem.IO.Directoryクラスを、ファイルのサイズを取得するにはSystem.IO.FileInfoクラスを使います。そして、フォルダ以下のすべてのファイルを取得するには再帰を使う必要があるでしょう。

出来上がったコードはこちら。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace GetTotal
{
  class Program
  {
    static void Main(string[] args)
    {
      if(args.Length < 1)
      {
        Console.Error.WriteLine("Less argument.");
        return;
      }

      try
      {
        Func<string, long> gettotal = null;
        gettotal = p =>
            Directory.GetDirectories(p).Select(f => gettotal(f)).Sum() +
            Directory.GetFiles(p).Select(f => new FileInfo(f).Length).Sum();
        Console.WriteLine("Size: {0:#,0} Bytes", gettotal(args[0]));
      }
      catch(Exception e)
      {
          Console.Error.WriteLine(e.Message);
      }
    }
  }
}

VS2012のプログラムフォルダで試してみました。


うん。うまくいった。


でも、21~24行目は本当はこう書きたかった。

Func<string, long> gettotal = p => 
    Directory.GetDirectories(p).Select(f => gettotal(f)).Sum() +
    Directory.GetFiles(p).Select(f => new FileInfo(f).Length).Sum();

『未割当のローカル変数 'gettotal' が使用されました。』と怒られるのでした。仕方ない。ぐぅ。

2013年3月12日火曜日

Googleサイト

Googleサイトってのもあるのね。ここで書こうとしている内容からすると、こっちのほうが向いていたかもしれないな。

ひとまずBloggerで書き溜めておいて、溜まってきて、さらに気が向いたら移すことを考えてみようかな。

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が大変賢く、ラムダ式の中でもちゃんと効いてくれるため、楽に使うことができます。

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

『よつばと!』12巻

大変お待たされました!遂に発売。冬っぽい表紙。


2013年3月9日土曜日

WBC 日本ー台湾戦

いい試合でした。見ていて面白かった。9回表の攻撃はドキドキしたよ。

2013年3月8日金曜日

LINQ(Enumerable拡張メソッド)を利用したシンプルなリファクタリング・カタログ


VisualStudio 2008の登場以降、C#のコーディングにLINQが使えるようになり、「.netでのコーディングの仕方」がLINQの有り無しで大きく変化しました。が、今年が2013年なのですでに5年が経ちますが、まだまだLINQに馴染みが無い人も多いようです。

そこで、便利で使える局面が多いEnumerable拡張メソッドを使って、シンプルなコードを書く方法を、局面別にカタログスタイルで書いてみます。

Enumerable拡張メソッドは、以下のときに使用できます。
  • .net Framework 3.5以上を使っている。
  • System.Core.dllを参照設定している。
  • "using System.Linq;"でSystem.Link名前空間を参照している。
VisualStudio 2008以降で普通にプロジェクトを作れば、最初から上記条件を満たすプロジェクトになります。

01.コレクション内のすべての要素の合計を算出する


従来

public int Sum(IEnumerable<int> col)
{
  int sum = 0;
  foreach (int item in col)
  {
    sum += item;
  }
  return sum;
}

Enumerable+ラムダ式

public int Sum(IEnumerable<int> col)
{
  return col.Sum();
}

02.コレクション内のすべての要素の特定メンバの最小値を算出する


従来

public int Min(IEnumerable<Item> col)
{
  int min = int.MaxValue;
  foreach (Item item in col)
  {
    if (item.Cond < min)
      min = item.Cond;
  }
  return min;
}

Enumerable+ラムダ式

public int Min(IEnumerable<Item> col)
{
  return col.Select(item => item.Cond).Min();
}

03.コレクション内のすべての要素の文字数の平均を算出する


従来

public double StrLenAvg(IEnumerable<Item> col)
{
  double sum = 0.0;
  foreach (Item item in col)
  {
    sum += item.Str.Length;
  }
  return sum / col.Count();
}

Enumerable+ラムダ式

public double StrLenAvg(IEnumerable<Item> col)
{
  return col.Select(item => (double)item.Str.Length).Average();
}

04.コレクション内の条件に合致する要素の数を数える


従来

public int CountOf(IEnumerable<Item> col, int target)
{
  int count = 0;
  foreach (Item item in col)
  {
    if (item.Cond == target)
    {
      ++count;
    }
  }
  return count;
}

Enumerable+ラムダ式

public int CountOf(IEnumerable<Item> col, int target)
{
  return col.Count(item => item.Cond == target);
}

05.コレクション内にある条件に合致する要素が存在するかを確認する


従来

public bool CheckAny(IEnumerable<Item> col, int target)
{
  bool exists = false;
  foreach (Item item in col)
  {
    if (item.Cond == target)
    {
      exists = true;
      break;
    }
  }
  return exists;
}

Enumerable+ラムダ式

public bool CheckAny(IEnumerable<Item> col, int target)
{
  return col.Any(item => item.Cond == target);
}

06.コレクション内のすべての要素がある条件に合致するかを確認する


従来

public bool CheckAll(IEnumerable<Item> col, int target)
{
  bool all = true;
  foreach (Item item in col)
  {
    if (item.Cond != target)
    {
      all = false;
      break;
    }
  }
  return all;
}

Enumerable+ラムダ式

public bool CheckAll(IEnumerable<Item> col, int target)
{
  return col.All(item => item.Cond == target);
}

07.コレクション内のある条件に合致する要素のみ特定の処理を行う


従来

public void SetValueToAllOf(IEnumerable<Item> col, int target, int val)
{
  foreach (Item item in col)
  {
    if (item.Cond == target)
    {
      item.Val = val;
    }
  }
}

Enumerable+ラムダ式

public void SetValueToAllOf(IEnumerable<Item> col, int target, int val)
{
  foreach (Item item in col.Where(i => i.Cond == target))
  {
    item.Val = val;
  }
}

08.コレクション内のある条件に最初に合致する要素のみ特定の処理を行う


従来

public void SetValueToFirst(IEnumerable<Item> col, int target, int val)
{
  foreach (Item item in col)
  {
    if (item.Cond == target)
    {
      item.Val = val;
      break;
    }
  }
}

Enumerable+ラムダ式

public void SetValueToFirst(IEnumerable<Item> col, int target, int val)
{
  Item item = col.FirstOrDefault(i => i.Cond == target);
  if(item != null)
  {
    item.Val = val;
  }
}

Enumerable+ラムダ式 (特殊なケース)


col内に条件に合致するものが存在することがわかっていて、仮に存在していない場合は例外がスローされても構わない場合はこう。

public void SetValueToFirst(IEnumerable<Item> col, int target, int val)
{
  col.First(i => i.Cond == target).Val = val;
}

09.コレクション内のすべての要素の、特定メンバが最大の要素を取得する


従来

public Item MaxItemOf(IEnumerable<Item> col)
{
  Item maxitem = new Item() { Cond = int.MinValue };
  foreach (Item item in col)
  {
    if (item.Cond > maxitem.Cond)
    {
      maxitem = item;
    }
  }
  return maxitem;
}

Enumerable+ラムダ式

public Item MaxItemOf(IEnumerable<Item> col)
{
  return col.OrderByDescending(item => item.Cond).First();
}

10.コレクション内のコレクションを列挙する


従来

public string EnumLayered(IEnumerable<LayeredItem> colcol)
{
  foreach (var col in colcol)
  {
    foreach (var item in col.Items)
    {
      Console.WriteLine(item.Str);
    }
  }
}

Enumerable+ラムダ式

public string EnumLayered(IEnumerable<LayeredItem> colcol)
{
  foreach (var item in colcol.SelectMany(col => col.Items))
  {
    Console.WriteLine(item.Str);
  }
}

食べ超

@ITの、 「一生食べられるエンジニアになるための超IT用語解説」、略して「食べ超」。これがイイ!同業者さんなら、うっかり仕事中に見ると大変なことになるレベル。おススメです。

せっかくだから、良いコードを書きたい

では、良いコードとはどんなのか?

僕の感覚だと、読みやすく、シンプルで、バグの入り込みにくいコード。つまり、『メンテナンス性の高いコード』だと思ってる。

『メンテナンス性の高いコード』を書くためには、何かしらのコーディングスタンダードに準ずるのも一つの手だけど、あんまり厳しいスタンダードは開発者の手足を縛ることにしかならないので、個人的にはあまり好きくない。たまに酷いのもあるし。

そこで、以下の3つのことを常に意識してみる。という位ではどうかな。


  • (意図した動作をしている限り、)短いは正義。
  • ブロックは少なく、ネストは浅く、スコープは短く。
  • 名前は重要。超重要。内容を的確に表し、十分短く、省略しない名前を付ける。

2013年3月7日木曜日

今週のマガジン

『ベイビーステップ』。意外と早くエーちゃんのターン。でもこのまますんなりとは勝たないだろーね。どうなるかな?

なんでここにきてブログ開設?

理由はいくつかあります。


  • 会社でそれなりに長いことお仕事していて、情報共有のためにWikiを立ててそこに技術的なネタを書いたりしてるんですが、いかんせん読んでくれる人が少ない。も少し張り合いがほしい。
  • それならイントラ内で見えるところに書けってなもんですが、社内SNSはあるけど社内ブログはない。
  • 会社内でそういった情報を探す人は、たぶん社外のサイトを探すだろうから、社外にブログを立てたほうが、社内の人も探しやすいんじゃないかと考えた。矛盾してるけどね。
  • お仕事(プログラム関係)で書きたいネタが多いのだけど、ここ最近、この仕事の将来をちょっと悲観的に考えていて、この手のネタを出してもしょうがないかなー。などと思っていた。でも、身近で割とニーズがありそうな感触があった。
  • プログラム言語としてはC#を好んで使っているのだけど、「@IT」の記事「連載:業開中心会議議事録: 第1回 .NET技術の断捨離」の中で、パネルディスカッションの観客の半数以上が、LINQやラムダ式を使っていない。という記事があって軽く衝撃を受けた。あんな便利なものを!
  • んで、考えてみると、今まで自分が分からなかったり壁に当たった場合、他の先駆者の記したブログ等から解決方法が得られたりすることが多かった。つまり、自分は誰かが苦労した結果をつまみ食いしてるだけなんだなー。情報を消費するばっかりなんだなー。と。
  • だから、お仕事でお金もらって苦労したことといっても、少しは社会に還元すべきじゃないのかな。と考えた。
  • とはいえ、ブログの形にしてもそんなに提供可能なネタはないよー。とも思ったけど、その時はその時。漫画の感想ブログになってもいいんじゃね?別にお仕事関係以外のことをポストしてもいいしね。と考えたら少し気が楽になった。
そんなことを考えて、昨日ブログの開設に至りました。

事前にちょっとだけ予防線を張っておきます。

プログラム関連のエントリをポストすることが多いと思いますが、多分間違ったことも書いちゃうと思います。誤りが判明すれば訂正しますが、書かれていることをすべて信用しないでくださいね。

それと、漫画に関連するエントリもポストすると思います。言うまでもないですがあくまで僕個人の感想です。意見が違っても怒んないでくださいね。


さて、エントリはおいおい書いていきます。のんびり行きますよ。

ここらで軽く自己紹介なんぞ


  • 自己紹介…。それなりに年のいったおっさんです。
  • 漫画が大好き。少年誌も青年誌もいろいろ読んでます。
  • 歴史小説。特に戦国時代のお話が大好きです。最近は幕末あたりにも多少興味が出てきました。
  • 作家さんとしては池波正太郎、司馬遼太郎、隆慶一郎が好き。
  • お仕事は…システム屋さんというのが近いかな。システムを作るために、日々プログラムを書いたり、他のいろんなことをしたりしています。
  • 仕事の中ではプログラムを書くのが好き。こう、何かを作り上げる感じがいいんだと思う。
こんなところでしょうか。こういうことはプロフィールに書けと自分でも思うのですが、こう、そのうち埋もれてしまうぐらいがちょうどいいかなー。と。

2013年3月6日水曜日

今週のスピリッツ

新連載の『王様達のヴァイキング』が初回からかなり面白かった。今後に期待大。

ただし、ハッカーネタは漫画のテーマとして難しいほうじゃないかな。一歩間違うと途端にうそくさくなるので。そこら辺をどう描いていくかも期待して読んでいきたいですな。

ごあいさつ

ちょっと思うところがあり、ブログを開設してみました。

もともとが面倒くさがりなので、どれだけ続くものかわかりませんが。

内容としては、たぶん漫画のこととお仕事関係のことがメインになると思いますが、とくにジャンルを決めずに、思いついたことが書ければいいなと思います。

よろしくお願いします。