4. 汎用的なアルゴリズムの分離
さて、前述のプログラムをもう一度よく眺めてみましょう。 互いに独立な2つの関数をRun関数から分離できたように見えます。
ですが、この2つの関数は、次のようなものに依存しているので、汎用的なアルゴリズムを分離したとは言い難いのです。
- コレクションの要素の型に依存している
- コレクションの構造(リストか配列かなど)に依存している
- 抽出条件が何であるかに依存している
- それぞれの要素をどうするかに依存している
これでは、この2つの関数は、この文脈でしか使うことができません。即ち、「抽出(Filter)」は、書籍のリストの中から、「書籍のタイトルまたは出版社の名前に "リ" を含むもの」を抽出するのにしか使えませんし、「表示(Show)」は、書籍リストのそれぞれの書籍を表示するのにしか使うことができません。
もっと汎用的なアルゴリズムを切り出すことはできないでしょうか? もっと汎用的に抽出のアルゴリズムや繰り返し処理のアルゴリズムは書けないものでしょうか?
もし仮に、C++でそれができないとすると、C++には汎用的なアルゴリズムの記述能力がないことになりますが、もちろんそんなことはありません。
これらの依存を関数から分離して、汎用的なアルゴリズムにするには、例えば次のようにすれば良いのです。
| 番号 | 依存の内容 | 戦略 |
|---|---|---|
| 1 | コレクションの要素の型に依存している | ジェネリックプログラミング(C++ではtemplateの使用) |
| 2 | コレクションの構造(リストか配列かなど)に依存している | ジェネリックプログラミング(C++ではtemplateの使用)とイテレーターパターン |
| 3 | 抽出条件が何であるかに依存している | 抽出条件を関数で表し、それを渡す |
| 4 | それぞれの要素をどうするかに依存している | どうするかを関数で表し、それを渡す |
これらを使って、2つの関数を次のような汎用的なアルゴリズムにしてみましょう。
| before(様々なものに依存した処理) | after(汎用的なアルゴリズム) |
|---|---|
| 書籍のリストの中から、「書籍のタイトルまたは出版社の名前に "リ" を含むもの」を抽出する | コレクションから或る条件に合致した要素だけを抽出する |
| 書籍リストのそれぞれの書籍を表示する | コレクションのそれぞれの要素に対して特定のことをする |
実際にやってみると、例えば次のようになります。
「コレクションから或る条件に合致した要素だけを抽出する」AppendIf 関数と、「コレクションのそれぞれの要素に対して特定のことをする」ForEach 関数
#pragma once
namespace MyCollection {
// コレクションから或る条件に合致した要素だけを抽出する
template<class IteraterType, class FunctionType, class DestinationType>
inline void AppendIf(
IteraterType first , // コレクションの先頭を指すイテレーター
IteraterType last , // コレクションの末尾の次を指すイテレーター
DestinationType& destination, // 抽出した要素の追加先のコレクション
FunctionType isMatch // 抽出条件を表した関数
)
{
for (; first != last; ++first) {
if (isMatch(*first))
destination.push_back(*first);
}
}
// コレクションから或る条件に合致した要素だけを抽出する
template<class SourceType, class FunctionType, class DestinationType>
inline void AppendIf(
SourceType source , // コレクション
DestinationType& destination, // 抽出した要素の追加先のコレクション
FunctionType isMatch // 抽出条件を表した関数
)
{ AppendIf(source.begin(), source.end(), destination, isMatch); }
// コレクションのそれぞれの要素に対して特定のことをする
template<class IteraterType, class FunctionType>
inline void ForEach(
IteraterType first , // コレクションの先頭を指すイテレーター
IteraterType last , // コレクションの末尾の次を指すイテレーター
FunctionType action // それぞれの要素に行うことを表した関数
)
{
for (; first != last; ++first)
action(*first);
}
// コレクションのそれぞれの要素に対して特定のことをする
template<class CollectionType, class FunctionType>
inline void ForEach(
CollectionType collection, // コレクション
FunctionType action // それぞれの要素に行うことを表した関数
)
{ ForEach(collection.begin(), collection.end(), action); }
}
関心事の分離
これらの関数は、ほぼそのアルゴリズムだけを記述しています。アルゴリズムで必要な部分だけを記述し、アルゴリズムではない部分をあまり記述していません。
そのアルゴリズムという関心事を高凝集に分離したことになります。
また、他への依存も少ないです。要素の型やコレクションの種類、抽出条件や各要素に対してやることを、それほど選びません。
そのため、これらのアルゴリズムを様々なプログラムから利用できるはずです。
5. 汎用的なアルゴリズムの試用
では、これらを元のプログラムで使ってみる前に、これらのC++の関数として書かれたアルゴリズムが本当に汎用的に使えるかどうか試してみましょう。
intのlistの中から偶数だけをコンソールに表示する
まず、intのlistの中から偶数を抽出し、コンソールに表示してみます。
| 要素の型 | int |
|---|---|
| コレクション | list |
| 抽出条件 | IsEven(偶数であること) |
| 各要素に対してやること | Show(コンソールに表示) |
#include <iostream>
#include <list>
#include <string>
using namespace std;
#include "MyCollection.h"
using namespace MyCollection;
class Program
{
public:
void Run()
{
list<int> collection = { 1, 4, 9, 16, 25 }; // int の list
list<int> selectedCollection;
// 汎用的なアルゴリズムを利用
AppendIf(collection, selectedCollection, IsEven); // 偶数だけを抽出
ForEach(selectedCollection, Show<int>); // 抽出されたそれぞれの int を表示
}
private:
static bool IsEven(int number)
{ return number % 2 == 0; }
template <class T>
static void Show(T item)
{ wcout << item << endl; }
};
int main()
{
Program().Run();
return 0;
}
実行結果は、次の通りです。うまくいきました。
4 16
double の配列の中から正数だけを表示する
コレクションの構造や要素の型、抽出条件を変えてみましょう。
| 要素の型 | double |
|---|---|
| コレクション | 配列 |
| 抽出条件 | IsPositive(正数であること) |
| 各要素に対してやること | Show(コンソールに表示) |
#include <iostream>
#include <vector>
#include <string>
using namespace std;
#include "MyCollection.h"
using namespace MyCollection;
class Program
{
public:
void Run()
{
// double の配列
double array [] = { -3.0, 0.1, -0.04, 0.001, -0.0005, 0.00009 };
vector<double> selectedCollection;
// 汎用的なアルゴリズムを利用
AppendIf(array, array + sizeof(array) / sizeof(array[0]),
selectedCollection, IsPositive); // 正数だけを抽出
ForEach(selectedCollection, Show<double>); // 抽出されたそれぞれの double を表示
}
private:
static bool IsPositive(double number)
{ return number > 0; }
template <class T>
static void Show(T item)
{ wcout << item << endl; }
};
int main()
{
Program().Run();
return 0;
}
今度の実行結果は、次の通りです。こちらもうまくいくようです。
0.1 0.001 9e-005
ここまでは、汎用的なアルゴリズムとして利用できているように見えます。
