4. 汎用的なアルゴリズムの分離
さて、前述のプログラムをもう一度よく眺めてみましょう。互いに独立な2つのメソッドをMainメソッドから分離できたように見えます。
ですが、この2つのメソッドは、次のようなものに依存しているので、汎用的なアルゴリズムを分離したとは言い難いのです。
- コレクションの要素の型に依存している
- コレクションの構造(リストか配列かなど)に依存している
- 抽出条件が何であるかに依存している
- それぞれの要素をどうするかに依存している
これでは、この2つのメソッドは、この文脈でしか使うことができません。すなわち、「抽出(Filter)」は、書籍のリストの中から、「書籍のタイトルまたは出版社の名前に "リ" を含むもの」を抽出するのにしか使えませんし、「表示(Show)」は、書籍リストのそれぞれの書籍を表示するのにしか使うことができません。
もっと汎用的なアルゴリズムを切り出すことはできないでしょうか? もっと汎用的に抽出のアルゴリズムや繰り返し処理のアルゴリズムは書けないものでしょうか?
もし仮に、C#でそれができないとすると、C#には汎用的なアルゴリズムの記述能力がないことになりますが、もちろんそんなことはありません。
これらの依存をメソッドから分離して、汎用的なアルゴリズムにするには、例えば次のようにすれば良いのです。
番号 | 依存の内容 | 戦略 |
---|---|---|
1 | コレクションの要素の型に依存している | ジェネリックプログラミング |
2 | コレクションの構造(リストか配列かなど)に依存している | ジェネリックプログラミングとイテレーターパターン(C#ではIEnumerable<T>の利用) |
3 | 抽出条件が何であるかに依存している | 抽出条件をメソッドで表し、それをdelegateとして渡す |
4 | それぞれの要素をどうするかに依存している | どうするかをメソッドで表し、それをdelegateとして渡す |
これらを使って、2つのメソッドを次のような汎用的なアルゴリズムにしてみましょう。
before(さまざまなものに依存した処理) | after(汎用的なアルゴリズム) |
---|---|
書籍のリストの中から、「書籍のタイトルまたは出版社の名前に "リ" を含むもの」を抽出する | コレクションから或る条件に合致した要素だけを抽出する |
書籍リストのそれぞれの書籍を表示する | コレクションのそれぞれの要素に対して特定のことをする |
実際にやってみると、例えば次のようになります。
「コレクションから或る条件に合致した要素だけを抽出する」Filterメソッドと、「コレクションのそれぞれの要素に対して特定のことをする」ForEachメソッド
using System; using System.Collections.Generic; namespace MyCollection { public static class Enumerable { // コレクションから或る条件に合致した要素だけを抽出する public static IEnumerable<T> Filter<T>( this IEnumerable<T> collection, // コレクション Func<T, bool> isMatch // 抽出条件 ) { foreach (var item in collection) { if (isMatch(item)) yield return item; } } // コレクションのそれぞれの要素に対して特定のことをする public static void ForEach<T>( this IEnumerable<T> collection, // コレクション Action<T> action // それぞれの要素に行うこと ) { foreach (var item in collection) action(item); } } }
関心事の分離
これらのメソッドは、ほぼそのアルゴリズムだけを記述しています。アルゴリズムで必要な部分だけを記述し、アルゴリズムではない部分をあまり記述していません。
そのアルゴリズムという関心事を高凝集に分離したことになります。
また、他への依存も少ないです。要素の型やコレクションの種類、抽出条件や各要素に対してやることを、それほど選びません。
そのため、これらのアルゴリズムをさまざまなプログラムから利用できるはずです。
5. 汎用的なアルゴリズムの試用
では、これらを元のプログラムで使ってみる前に、これらのC#のメソッドとして書かれたアルゴリズムが本当に汎用的に使えるかどうか試してみましょう。
intのListの中から偶数だけをコンソールに表示する
まず、intのListの中から偶数を抽出し、コンソールに表示してみます。
要素の型 | int |
---|---|
コレクション | List |
抽出条件 | IsEven(偶数であること) |
各要素に対してやること | Show(コンソールに表示) |
using MyCollection; using System; using System.Collections.Generic; class Program { static void Main() { var collection = new List<int> { 1, 4, 9, 16, 25 }; // int の List // 汎用的なアルゴリズムを利用 collection.Filter (IsEven) .ForEach(Show ); // 抽出されたそれぞれの int を表示 } static bool IsEven(int number) { return number % 2 == 0; } static void Show<T>(T item) { Console.WriteLine(item); } }
実行結果は、次のとおりです。うまくいきました。
4 16
doubleの配列の中から正数だけを表示する
コレクションの構造や要素の型、抽出条件を変えてみましょう。
要素の型 | double |
---|---|
コレクション | 配列 |
抽出条件 | IsPositive(正数であること) |
各要素に対してやること | Show(コンソールに表示) |
using MyCollection; using System; using System.Collections.Generic; class Program { static void Main() { // double の配列 var collection = new List<double> { -3.0, 0.1, -0.04, 0.001, -0.0005, 0.00009 }; // 汎用的なアルゴリズムを利用 collection.Filter (IsPositive) .ForEach(Show ); // 抽出されたそれぞれの double を表示 } static bool IsPositive(double number) { return number > 0; } static void Show<T>(T item) { Console.WriteLine(item); } }
今度の実行結果は、次のとおりです。こちらもうまくいくようです。
0.1 0.001 9e-005
ここまでは、汎用的なアルゴリズムとして利用できているように見えます。