DbCommandの取扱いも分離する
前よりはだいぶ見通しが良くなりました。それでも、usingの入れ子が三重にもなっているのは鬱陶しいものです。ロジックの中でDbCommandを扱っていますが、それに意味はあるのでしょうか? そこもDbCommandHelperに移してしまえばよさそうです。テストコードと改良したDbCommandHelperは掲載しませんが、検索するメソッドは次のようにリファクタリングできます。詳細はサンプルコードをご覧ください。
DbCommandも使わなくなった前方一致で姓名を検索する()メソッド
public static IList<Customer> 前方一致で姓名を検索する(string head) {
var list = new List<Customer>();
if (string.IsNullOrWhiteSpace(head))
return list;
using (DbCommandHelper改3 helper = new DbCommandHelper改3()) {
var cmdText =
@"SELECT Country, [Company Name], [Customer ID], [Contact Name]
FROM Customers
WHERE ([Contact Name] LIKE @p1) OR ([Contact Name] LIKE @p2)";
var param1 = head + "%";
var param2 = "% " + head + "%";
using (DbDataReader reader = helper.ExecuteReader(cmdText, param1, param2)) {
while (reader.Read()) {
var customer = new Customer() {
Country = reader.GetString(0),
CompanyName = reader.GetString(1),
CustomerID = reader.GetString(2),
ContactName = reader.GetString(3),
};
list.Add(customer);
}
}
}
return list;
}
DbDataReaderをモックで置き換えるのは面倒
上のコードでDbDataReaderを受け取って何をしているかというと、whileループで検索結果をCustomerオブジェクトに詰め替えているだけです。DbDataReaderをモックやダミー実装に置き換えるのは、とても面倒なので、このままではユニットテストすべてで実際のデータベースへのアクセスが必要になってしまいそうです。
DbCommandHelperには、DbDataReaderではなく、IEnumerable<Customer>を返してもらうようにしておきましょう。モックやダミーに置き換えるのが、とても楽になります。テストコードは省略しますので、詳しくはサンプルコードをご覧ください。
IEnumerable<Customer>を返すようにしたDbCommandHelperクラス
public class DbCommandHelper改4 : IDisposable {
private SqlCeConnection _connection;
internal SqlCeConnection Connection {
get {
// 【省略】
}
}
internal SqlCeCommand Command { get; private set; }
internal DbCommand CreateCommand(string commandText) {
this.Command = new SqlCeCommand(commandText, Connection);
return this.Command;
}
internal void AddStringParameter(DbCommand cmd, string paramName, string parameter) {
// 【省略】
}
internal DbDataReader ExecuteReader(string cmdText, string param1, string param2) {
this.CreateCommand(cmdText);
this.AddStringParameter(this.Command, "@p1", param1);
this.AddStringParameter(this.Command, "@p2", param2);
return this.Command.ExecuteReader();
}
public IEnumerable<Customer> FindCustomers(string cmdText, string param1, string param2) {
var results = new List<Customer>();
using (DbDataReader reader = this.ExecuteReader(cmdText, param1, param2)) {
while (reader.Read()) {
var customer = new Customer() {
Country = reader.GetString(0),
CompanyName = reader.GetString(1),
CustomerID = reader.GetString(2),
ContactName = reader.GetString(3),
};
results.Add(customer);
}
}
return results;
}
public void Dispose() {
// 【省略】
}
}
ご覧のとおり、DbCommandHelperクラスがずいぶん肥大化してしまい、また、名前もふさわしくなくなってきたので、こちらもそろそろリファクタリングが必要です(本稿ではここまでとします)。
製品コードは、リファクタリングして次のようになります。
DbDataReaderも使わなくなった前方一致で姓名を検索する()メソッド
public static IList<Customer> 前方一致で姓名を検索する(string head) {
if (string.IsNullOrWhiteSpace(head))
return new List<Customer>();
using (DbCommandHelper改4 helper = new DbCommandHelper改4()) {
var cmdText =
@"SELECT Country, [Company Name], [Customer ID], [Contact Name]
FROM Customers
WHERE ([Contact Name] LIKE @p1) OR ([Contact Name] LIKE @p2)";
var param1 = head + "%";
var param2 = "% " + head + "%";
return helper.FindCustomers(cmdText, param1, param2).ToList<Customer>();
//※ using System.Linq; が必要
//※ なお、LINQ を有効に使うには、ここでは ToList<Customer>() せずに返した方が良い。
}
}
これで、最初に挙げた問題点の2つめ「ロジックは、できるだけ環境に依存しないコードにしたい」は、だいたい解消できたでしょう。筆者の主観としては、まだロジックがSQL Serverに依存していることが気になります。SQL文を扱う部分も切り出して、ロジック→SQL文ハンドリング→データベースアクセスという三層構造にしたいと思うところですが、そこはケースバイケースだと思います。
このあと、FindCustomers()メソッドを持つインターフェースを定義して、DIコンテナを使うなどして実装をすげかえられるようにするのは、難しくないでしょう。3つめの問題点「モックやダミーに置き換えるのが困難」も、解消できています。
モックやダミーに置き換えやすい構造にする
今回のサンプルで示したビジネスロジックは検索を1回やっているだけですが、通常はビジネスロジックの一連の流れの中で何回もデータベースへのアクセスがあるでしょう。それぞれのアクセスを、個別にモックやダミーに置き換えられるように作っていくと、ロジックのTDDがはかどります。
この考え方は、データベースへのアクセス方法がなんであろうと、変わりません。LINQ to SQLだろうとEntity Frameworkだろうと、ビジネスロジックの処理の流れからデータベースへのアクセス部分を切り離せるように作っていくとよいです。
