2012年10月14日

CSVインポートをデザインパターンで実装してみる

今回はデザインパターンを使ってCSVのインポート機能を実装してみます。デザインパターンという名前は有名ですが、どんなときに使ったらいいのかが悩みどころ。

サンプルコードはC#4.0+.NET Framework 4で実装していますが、古いバージョンでもたぶん動きます。VB使いの人は・・・、適当に読み替えてください。



仕様

画面のボタンを押したらユーザーのデータが書かれたCSVファイルを読み込み、それをUserデータクラスのコレクション(今回はList<User>)に格納する。CSVの中身は以下のような感じ。

01,ももこ,8
02,ひろし,40
03,すみれ,40
04,さきこ,11
05,ともぞう,76



とりあえず実装

とりあえず仕様を満たすように実装してみましょう。特に何の工夫もなく、以下のような感じで。

とりあえず実装
//ボタンのイベントハンドラ
private void button1_Click(object sender, EventArgs e)
{
string fileName = "C:\\user.csv";
List<User> userList = new List<User>();
using (TextFieldParser parser = new TextFieldParser(fileName))
{
parser.Delimiters = new string[] { "," };
while (!parser.EndOfData)
{
string[] fields = parser.ReadFields();
User user = new User();
user.UserCode = fields[0];
user.UserName = fields[1];
user.Age = Convert.ToInt32(fields[2]);
userList.Add(user);
}
}
}

//データクラス
public class User
{
public string UserCode { get; set; }
public string UserName { get; set; }
public int Age { get; set; }
}



CSVの読み込みにはTextFieldParserクラスを使っています。こいつはMicrosoft.VisualBasic.FileIO名前空間のクラスなので、C#から使うときはusingでインポートしましょう。Userクラスはユーザーのデータを格納する単なる入れ物です。

とにかく仕様どおりに動くプログラムができました。それではケチをつけていきます。
 ・CSVインポート機能って他でも使いそうなので、Formクラスに書きたくない。
 ・「CSVを読み込む処理」と「データクラスに詰め直す処理」が同じ関数内に書かれてて何か嫌。
 ・他のCSV(例えば住所とか)もインポートするようになったら、冗長な記述が増えそう。




Strategyパターンで実装してみる

Strategyパターンとは、ロジックを別クラスに切り出して簡単に差し替えられるようにしようよ、ってパターンです。

Strategyパターンで実装
//ボタンのイベントハンドラ
private void button1_Click(object sender, EventArgs e)
{
string fileName = "C:\\user.csv";
//ロジックの差し替えポイント。
CsvImporter<User> csv = new CsvImporter<User>(new UserCsvAnalysis());
List<User> userList = csv.Import(fileName);
}

//データクラス(ユーザー)
public class User
{
public string UserCode { get; set; }
public string UserName { get; set; }
public int Age { get; set; }
}

//データクラスへの詰め替え機能を司るインターフェイス
public interface ICsvAnalysisStrategy<T>
{
T Analyze(string[] csvFields);
}

//ユーザーCSVの1行をデータクラスに詰め替えるクラス
public class UserCsvAnalysis : ICsvAnalysisStrategy<User>
{
public User Analyze(string[] csvFields)
{
User user = new User();
user.UserCode = csvFields[0];
user.UserName = csvFields[1];
user.Age = Convert.ToInt32(csvFields[2]);
return user;
}
}

//CSVインポートの窓口的なクラス
public class CsvImporter<T>
{
private ICsvAnalysisStrategy<T> analyzer;

public CsvImporter(ICsvAnalysisStrategy<T> analyzer)
{
this.analyzer = analyzer;
}

public List<T> Import(string fileName)
{
List<T> list = new List<T>();
using (TextFieldParser parser = new TextFieldParser(fileName))
{
parser.Delimiters = new string[] { "," };
while (!parser.EndOfData)
{
list.Add(analyzer.Analyze(parser.ReadFields()));
}
}
return list;
}
}



Strategyパターンの肝としては、ロジックを司るクラス(ここではデータ詰め替え用のUserCsvAnalysisクラス)にインターフェイスを付けておきます。インターフェイスを付けた各クラスは「同じような物」として扱えます。「同じような物」だから簡単に差し替えられる、という理屈です。

コード中に出てくる「T」というのはデータクラスの型、ここではUserクラスのことです。ユーザーだけでなく、別のCSV(例えば住所とか)をインポートすることを見越して、ジェネリックで一般化しています。

では、さきほどケチをつけたポイントについて確認してみます。

・CSVインポート機能って他でも使いそうなので、Formクラスに書きたくない。
 →インポート機能をFormクラス以外に分離したのでどこからでも使えます。

・「CSVを読み込む処理」と「データクラスに詰め直す処理」が同じ関数内に書かれてて何か嫌。
 →「CSVを読み込む処理」はCsvImporter<T>クラス、「データクラスに詰め直す処理」はUserCsvAnalysisクラスに分離されてます。

・他のCSV(例えば住所とか)もインポートするようになったら、冗長な記述が増えそう。
 →この点については、実際に住所CSVもインポートできるように改造してみましょう。




住所CSVもインポートできるようにしてみる

前回からの変更点を赤文字にしています。

住所CSVもインポートできるようにしてみる
//ボタンのイベントハンドラ
private void button1_Click(object sender, EventArgs e)
{
string fileName = "C:\\user.csv";
//ロジックの差し替えポイント。ユーザー用と住所用を差し替える。
CsvImporter<Address> csv = new CsvImporter<Address>(new AddressCsvAnalysis());
List<Address> addressList = csv.Import(fileName);
}

//データクラス(ユーザー)
public class User
{
public string UserCode { get; set; }
public string UserName { get; set; }
public int Age { get; set; }
}

//データクラス(住所)
public class Address
{
public string ZipCode { get; set; }
public string Prefecture { get; set; }
public string City { get; set; }
public string StreetNumber { get; set; }
}


//データクラスへの詰め替え機能を司るインターフェイス
public interface ICsvAnalysisStrategy<T>
{
T Analyze(string[] csvFields);
}

//ユーザーCSVの1行をデータクラスに詰め替えるクラス
public class UserCsvAnalysis : ICsvAnalysisStrategy<User>
{
public User Analyze(string[] csvFields)
{
User user = new User();
user.UserCode = csvFields[0];
user.UserName = csvFields[1];
user.Age = Convert.ToInt32(csvFields[2]);
return user;
}
}

//住所CSVの1行をデータクラスに詰め替えるクラス
public class AddressCsvAnalysis : ICsvAnalysisStrategy<Address>
{
public Address Analyze(string[] csvFields)
{
Address address = new Address();
address.ZipCode = csvFields[0];
address.Prefecture = csvFields[1];
address.City = csvFields[2];
address.StreetNumber = csvFields[3];
return address;
}
}


//CSVインポートの窓口的なクラス
public class CsvImporter<T>
{
private ICsvAnalysisStrategy<T> analyzer;

public CsvImporter(ICsvAnalysisStrategy<T> analyzer)
{
this.analyzer = analyzer;
}

public List<T> Import(string fileName)
{
List<T> list = new List<T>();
using (TextFieldParser parser = new TextFieldParser(fileName))
{
parser.Delimiters = new string[] { "," };
while (!parser.EndOfData)
{
list.Add(analyzer.Analyze(parser.ReadFields()));
}
}
return list;
}
}




どうでしょう?「CSVを読み込む処理」を司るCsvImporter<T>クラスは全く変更されていません。変更されているのはまさに「住所」に関する部分だけです。

ではまたケチを付けてみましょう。
CsvImporter<Address> csv = new CsvImporter<Address>(new AddressCsvAnalysis());
「ロジックの差し替えポイント」において「Address」と「AddressCsvAnalysis」が冗長です。住所用の詰め替えロジックを使ったら、戻り値は住所に決まってますから。ということで、「詰め替えロジックのインスタンスを作る処理」を別クラスに切り出します。いわゆるFactoryですね。「サルでもわかる 逆引きデザインパターン」を参考にしています。



Factoryクラスを作ってみる

前回からの変更点を赤文字にしています。

Factoryクラスを作ってみる
//ボタンのイベントハンドラ
private void button1_Click(object sender, EventArgs e)
{
string fileName = "C:\\user.csv";
//ロジックの差し替えポイント。
CsvImporter<Address> csv = new CsvImporter<Address>(/*引数を撤去*/);
List<Address> addressList = csv.Import(fileName);
}

//データクラス(ユーザー)
public class User
{
public string UserCode { get; set; }
public string UserName { get; set; }
public int Age { get; set; }
}

//データクラス(住所)
public class Address
{
public string ZipCode { get; set; }
public string Prefecture { get; set; }
public string City { get; set; }
public string StreetNumber { get; set; }
}

//データクラスへの詰め替え機能を司るインターフェイス
public interface ICsvAnalysisStrategy<T>
{
T Analyze(string[] csvFields);
}

//ユーザーCSVの1行をデータクラスに詰め替えるクラス
public class UserCsvAnalysis : ICsvAnalysisStrategy<User>
{
public User Analyze(string[] csvFields)
{
User user = new User();
user.UserCode = csvFields[0];
user.UserName = csvFields[1];
user.Age = Convert.ToInt32(csvFields[2]);
return user;
}
}

//住所CSVの1行をデータクラスに詰め替えるクラス
public class AddressCsvAnalysis : ICsvAnalysisStrategy<Address>
{
public Address Analyze(string[] csvFields)
{
Address address = new Address();
address.ZipCode = csvFields[0];
address.Prefecture = csvFields[1];
address.City = csvFields[2];
address.StreetNumber = csvFields[3];
return address;
}
}

//CSVインポートの窓口的なクラス
public class CsvImporter<T>
{
private ICsvAnalysisStrategy<T> analyzer;

public CsvImporter(/*引数を撤去*/)
{
this.analyzer = CsvAnalysisFactory<T>.Create();
}

public List<T> Import(string fileName)
{
List<T> list = new List<T>();
using (TextFieldParser parser = new TextFieldParser(fileName))
{
parser.Delimiters = new string[] { "," };
while (!parser.EndOfData)
{
list.Add(analyzer.Analyze(parser.ReadFields()));
}
}
return list;
}
}

//詰め替えロジックのインスタンスを作るクラス
public static class CsvAnalysisFactory<T>
{
public static ICsvAnalysisStrategy<T> Create()
{
if (typeof(T) == typeof(User))
{
return new UserCsvAnalysis() as ICsvAnalysisStrategy<T>;
}
else if (typeof(T) == typeof(Address))
{
return new AddressCsvAnalysis() as ICsvAnalysisStrategy<T>;
}
return null;
}
}




新設したCsvAnalysisFactory<T>クラスでは、ジェネリック引数Tによってどの詰め替えロジックを使うかを判断しています。以前は画面側で判断していたので、これで、画面側プログラマがどうやってインスタンスを作るか悩む必要がなくなります。今回は引数もプロパティも与えないので悩むこともありませんが・・・。この状態のクラス図は以下のようになります。

CSVインポートのクラス図


ここまででいったん完成としますが、試しに仕様を追加してみましょう。
・CSVの種類によってはヘッダー行が存在しています。
・ヘッダーのありなしはCSVの種類によって固定です。
・ヘッダー行はインポートしたくありません。
・ヘッダー行は1行目だけです。




ヘッダー行のありなしを実装してみる

前回からの変更点を赤文字にしています。

ヘッダー行のありなしを実装
//ボタンのイベントハンドラ
private void button1_Click(object sender, EventArgs e)
{
string fileName = "C:\\user.csv";
//ロジックの差し替えポイント。
CsvImporter<Address> csv = new CsvImporter<Address>();
List<Address> addressList = csv.Import(fileName);
}

//データクラス(ユーザー)
public class User
{
public string UserCode { get; set; }
public string UserName { get; set; }
public int Age { get; set; }
}

//データクラス(住所)
public class Address
{
public string ZipCode { get; set; }
public string Prefecture { get; set; }
public string City { get; set; }
public string StreetNumber { get; set; }
}

//データクラスへの詰め替え機能を司るインターフェイス
public interface ICsvAnalysisStrategy<T>
{
bool HasHeader { get; }
T Analyze(string[] csvFields);
}

//ユーザーCSVの1行をデータクラスに詰め替えるクラス
public class UserCsvAnalysis : ICsvAnalysisStrategy<User>
{
//ヘッダー行あり
public bool HasHeader { get { return true; } }

public User Analyze(string[] csvFields)
{
User user = new User();
user.UserCode = csvFields[0];
user.UserName = csvFields[1];
user.Age = Convert.ToInt32(csvFields[2]);
return user;
}
}

//住所CSVの1行をデータクラスに詰め替えるクラス
public class AddressCsvAnalysis : ICsvAnalysisStrategy<Address>
{
//ヘッダー行なし
public bool HasHeader { get { return false; } }

public Address Analyze(string[] csvFields)
{
Address address = new Address();
address.ZipCode = csvFields[0];
address.Prefecture = csvFields[1];
address.City = csvFields[2];
address.StreetNumber = csvFields[3];
return address;
}
}

//CSVインポートの窓口的なクラス
public class CsvImporter<T>
{
private ICsvAnalysisStrategy<T> analyzer;

public CsvImporter()
{
this.analyzer = CsvAnalysisFactory<T>.Create();
}

public List<T> Import(string fileName)
{
List<T> list = new List<T>();
using (TextFieldParser parser = new TextFieldParser(fileName))
{
parser.Delimiters = new string[] { "," };
if (analyzer.HasHeader)
{

//ヘッダーありの場合は1行読み飛ばす。
parser.ReadLine();
}

while (!parser.EndOfData)
{
list.Add(analyzer.Analyze(parser.ReadFields()));
}
}
return list;
}
}

//詰め替えロジックのインスタンスを作るクラス
public static class CsvAnalysisFactory<T>
{
public static ICsvAnalysisStrategy<T> Create()
{
if (typeof(T) == typeof(User))
{
return new UserCsvAnalysis() as ICsvAnalysisStrategy<T>;
}
else if (typeof(T) == typeof(Address))
{
return new AddressCsvAnalysis() as ICsvAnalysisStrategy<T>;
}
return null;
}
}




ヘッダーのありなしはCSVの種類に固有の話なので、詰め替え機能を司るインターフェイス(ICsvAnalysisStrategy<T>)にHasHeaderプロパティを追加しました。あとは、「ヘッダー行は読み込まない」という仕様なので、インポートの窓口クラス(CsvImporter<T>)で読み飛ばすように処理を追加しています。



まとめ


Strategyパターンはわりと使う機会がありそうです。使いどころとしては、if文で似たようなロジックに分岐しているところでしょうか。





posted by ぺるたご at 22:46| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。