インターフェイス その3
今回の内容
前回の終わりに次回はリフレクションを使って実装と書きましたが、その前にまず、値を単純に保持するだけのクラスとインターフェイスを作ってそれを読み込む処理を作ってみようと思います。
データを保持するクラスを追加
データ保持インターフェイスを定義
値を保持するだけのクラスが実装するインターフェイスをまず作ります。
interface IRow { void SetValues(IEnumerable<object> values); object this[int i] { get; } object this[string name] { get; } int FieldCount { get; } }
値を一括でセットするSetValuesメソッドと値を取得するデフォルトプロパティを実装しました。とりあえずIDataRecordインターフェイスと同じ感じにしておきました。
Rowクラスの実装
せっかくなので2種類作ります。
class FileRow : IRow { private List<object> Values = new List<object>(); public void SetValues(IEnumerable<object> values) { Values.Clear(); foreach(var value in values) { Values.Add(value); } } public object this[int i] { get { return Values[i]; } } public object this[string name] { get { throw new NotSupportedException(); } } public int FieldCount { get { return Values.Count; } } }
class TableRow : IRow { private Dictionary<string, object> Values = new[] { "Column1", "Column2", "Column3" }.ToDictionary(c => c, c => (object)null); public void SetValues(IEnumerable<object> values) { var keyValues = Values.Zip( values, (a, b) => new KeyValuePair<string, object>(a.Key, b) ); foreach(var kv in keyValues) { Values[kv.Key] = kv.Value; } } public object this[int i] { get { return Values.Values.Skip(i).First(); } } public object this[string name] { get { return Values[name]; } } public int FieldCount { get { return Values.Count; } } }
2つ作りました。1つ目はSetValuesで与えられた値一覧をそのまま保持するクラス、もう一つはあらかじめ項目名が決まっていて、決まった項目に値をセットするクラス。
2つ目のクラスのSetValuesメソッド内に出てくるZipメソッドはLINQの拡張メソッドで2つの一覧を短いほうに合わせて結合するメソッドです。
2つの要素に対してまとめて処理する際にとても便利なので使い方を覚えてしまうと良いと思います。
読み込み処理を記述
読み込み処理の抽象クラスを実装
読み込み処理の抽象クラスを作ります。
abstract class Reader<T> : IReader<T>, IReader where T : IRow { /// <summary> /// 生成関数 /// </summary> private Funccreator; /// <summary> /// コンストラクタ /// </summary> /// <param name="creator"></param> public Reader(Func<T> creator) { this.creator = creator; } /// <summary> /// オブジェクト生成 /// </summary> protected T CreateObject() { return creator(); } /// <summary> /// 実行 /// </summary> public abstract IEnumerable<T> Execute(); /// <summary> /// 実行(IReaderインターフェイス) /// </summary> IEnumerable IReader.Execute() { return Execute(); } }
このクラスはIReader<T>、IReaderインターフェイスを実装するように定義しています。
区別することができないメソッド定義「Execute」があるためIReader<T>インターフェイスがIReaderインターフェイスを継承するように定義することがません、そこで抽象クラスで両方を実装するようにまとめてしまいます。
クラスではインターフェイスを区別して実装するための仕組みがあります。
[インターフェイス名].[メソッド名]と記述することでインターフェイスのメソッドを区別して実装しています。
このようなかたちで実装するとこのオブジェクト(インスタンス)を利用するときにIReaderインターフェイスにキャストするとIReader.Executeメソッドが実行されますが、Reader<T>クラスにキャストすると普通に実装しているExecuteメソッドが実行されます。
そもそも違う動きをさせるつもりがないのでIReader.Executeメソッド内でも同じ動作になるように記述しています。
読み込み処理本体クラスを実装
IReader<T>抽象クラスを継承して処理本体を記述していきます。
1つ目はファイルを読み込むクラス
class FileReader<T> : Reader<T> where T : IRow { /// <summary> /// コンストラクタ /// </summary> /// <param name="creator">オブジェクト生成関数</param> public FileReader(Func<T> creator) : base(creator) { } public override IEnumerable<T> Execute() { using (StreamReader streamReader = new StreamReader(Properties.Settings.Default.FileName)) { while(!streamReader.EndOfStream) { var line = streamReader.ReadLine(); var values = line.Split(','); var obj = CreateObject(); obj.SetValues(values); yield return obj; } } } }
コンストラクタは抽象クラスにそのまま預けて、Execute内部では実行ファイルのSettingにあるファイルを読み込み、1行ごとインスタンスを生成し「,」で区切られた項目をインスタンスにセットして返却するように実装しています。
初めて「yield return」が現れましたが、特殊な返却処理です。
返却した内容は呼び出した側から見ると一覧(IEnumerable)になり、LINQの拡張メソッドが使用できます。
普通に記述するとループ構造が複雑になる場合に使える便利な書き方です。また、データを読み込む側と書き込む側でメソッドやクラスを分割するのに使用できるのでうまく分けると再利用性を上げたりできます。
もう一つはデータベース内のテーブルを読み込むクラス
class DbReader<T> : Reader<T> where T : IRow { /// <summary> /// コンストラクタ /// </summary> /// <param name="creator">オブジェクト生成関数</param> public DbReader(Func<T> creator) : base(creator) { } /// <summary> /// 読み込んだ内容をT型に変換して繰り返し返却 /// </summary> public override IEnumerable<T> Execute() { var factory = DbProviderFactories.GetFactory("Npgsql"); using (DbConnection connection = factory.CreateConnection()) { connection.ConnectionString = Properties.Settings.Default.ConnectionString; connection.Open(); using (DbCommand command = connection.CreateCommand()) { command.CommandText = "SELECT * FROM TABLE1"; using (DbDataReader dataReader = command.ExecuteReader()) { //DataReaderが値を取得しない場合ここで終了 if (!dataReader.HasRows) yield break; //DataReaderのフィールド名をLINQを使って列挙する //DataReaderのフィールド名は順序で取得するので順序をまず作成 var ordinals = Enumerable.Range(0, dataReader.FieldCount); while (dataReader.Read()) { //返却するオブジェクト生成 var obj = CreateObject(); var values = ordinals.Select(i => dataReader[i]); obj.SetValues(values); yield return obj; } } } } } }
ちょっと長くなっていますが、ファイルから読み込んでいる部分がテーブルから読み込んでいる以外の違いはありません。
Readerファクトリクラスを追加
ファクトリクラスでどんな読み込み処理を公開するか決めます。
static class ReaderFactory { public static IReader CreateReader(string readerName) { switch (readerName) { case "DbReader": return new DbReader<TableRow>(() => new TableRow()); case "FileReader": return new FileReader<FileRow>(() => new FileRow()); default: throw new InvalidOperationException("readerNameが無効です。"); } } }
引数でどの処理を行うか決まるように実装しています。実際使うときは設定を読み込んでそれに従って決めるとか、ユニットテストを行うときにはテスト用のReaderを生成するとか、いろいろここで変更するようにすればと思います。
利用する側の実装
最後に使う側を実装します。
class Program { static void Main(string[] args) { if (args.Length > 0) { IReader reader = ReaderFactory.CreateReader(args[0]); foreach(IRow obj in reader.Execute()) { for(int i = 0; i < obj.FieldCount; i++) { Console.Write("{0},", obj[i]); } Console.WriteLine(); } } else { Console.WriteLine("Readerをコマンドライン引数に書いてください"); } } }
見てもらうとわかるように、利用する側はReaderに関係するインターフェイスを利用しているけれど、インスタンスを参照していないので、それがどのように実装されているかわからなくても良い状況にできました。
まとめ
インターフェイスを使用することでクラス間の関わりを切り離しして実装方法に依存しないようにすることで、テストはそれぞれのクラスで行うことができるようになり保守性が上がります。また、処理を差し替えることが容易になり、機能の変更や拡張も行いやすい状態にできます。
何が言いたかったかというと「Interface segregation principle:インターフェース分離の原則」について触れておきたかったのでした。
今回の例だけではこの原則の全てを説明できていないですが、このような考えの理解に少しでも助けになればと思います。