#事の起こり
C++に合わせてオブジェクト指向も学習していましたが、いまいちオブジェクト指向のありがたみが分かりません。そこで、今回はありがたみを理解する為にデザインパターンの一つであるイテレータをC++で実装してみました。(本記事では、Javaで学ぶデザインパターン入門(著 結城 浩,2001年発行,第3版)を参考にしています。)
#デザインパターンについて、それを学ぶメリット
デザインパターンとはオブジェクト指向設計でよく使われる設計パターンのことです。要は、ソフトウェア設計界のベストプラクティスのことです。**「設計におけるベストとは何を指すのか?」と言われると私も答えに詰まりますが、私が勉強した限りではデザインパターンにおけるベストとは「部品の再利用性の高さ」であったり「修正,拡張性に富んでいること」**を指していることが多いように思います。したがって、デザインパターンを学べば再利用性と拡張性の高いソフトウェア設計の手助けになります。また、ソフトウェア設計における共通の概念である為、保守・改修において対象のソフトウェアの設計の意図を理解することにも役立ちます。(まだ、勉強したてなので半信半疑ではありますが・・・)
#イテレータとは?
イテレータはデザインパターンの一種で、グループを順番に指し示していき全体をスキャンする処理を行うもの(行う用のクラス)です。例えば、料理名が書かれたメニューブックの料理名を上から順番に表示する処理とかに応用できるデザインパターンです。
#イテレータを用いたサンプル① クラス構成
Javaで学ぶデザインパターン入門を元とに、私が試作したクラス図はこちらです。メニューブックというクラスに管理されたメニュー表のメニューをイテレータで前からスキャンするというものです。
構成を説明していきますね。(メンバ関数については後述)
①Menu
メニューブックに載るメニュー。メニュー名(name)をメンバにもつ
②MenuBook
メニューブック。各メニュー(Menuクラス)が登録されたブック(book)をメンバにもつ。また、登録されているメニューの総数(last)をメンバにもつ。今回のイテレータでスキャンされる対象となる
③Iterator
イテレータ。抽象クラス。イテレータの機能を一般化した抽象クラス。イテレータの実現に必要な関数(hasnextとnext)を持っている。
④MenuBookIterator。
メニューブックのためのイテレータ、MenuBookをスキャンする為のクラス。スキャンの為には、スキャン対象であるMenuBookの構成を知っている必要があるので、メニューブック(MenuBook)をメンバに持っている。また、iteratorが現在指し示しているメニューの番号(index)もメンバとしてもつ。
⑤Aggregate(Aggregateに関してはあまり理解できていませんので悪しからず)
集合体そのものを表す抽象クラス。ややこしいが、今回のMenuBookは複数のMenuをもつ集合体である為、Aggregateクラスを親クラスに持っている。(集合体を一般化したクラスってこと)
また、今回の例ではイテレーターを作りだすためのクラスでもあり、イテレータを作りだすためのメンバ関数myIteratorのインターフェースでもある。
#イテレータを用いたサンプル② 実装
今回はC++でイテレータパターンを実装してみました(実装に不備があれば是非ご指摘をいただきたい。)。
###①AggregateとIteratorクラス
JavaでいうInterfaceはC++では抽象クラスとして実装します。AggregateクラスとIteratorクラスの実装は下記の通りになります。どちらも抽象クラスなので、メソッドの実装は行いません。Aggregateはイテレータを生成する役割を持ったインターフェースでIteratorは実際のイテレータの機能を提供するインターフェースです。
class Aggregate{
public:
virtual Iterator* myIterator(void) = 0;//イテレータを返すメソッド
virtual ~Aggregate(){}//仮想デストラクタ
};
class Iterator{
public:
//hasNextとnextはイテレータが持つ実際の機能を実装する。
virtual bool hasNext() = 0;
virtual Menu next() = 0;
virtual ~Iterator(){}//仮想デストラクタ
};
###②Menuクラスの実装
Menuクラスの実装は下記の通りです。
class Menu{
private:
string name = "";//メニューの名前
public:
Menu(){}//デフォルトコンストラクタ
explicit Menu(const string& newName){//明示コンストラクタ
this->name = newName;
}
~Menu(){/*cout<<"Menuは解体された"<<endl;*/}//デストラクタ
void setName(const string& newName);
string getName(void);
};
各メソッドの実装は下記の通りです。setNameはMenuのnameをセットするアクセッサ(デフォルトコンストラクタがあるのでいらない気もしますが)、getNameはメニューの名前を返すメソッドになってます。次は、Menuを格納して管理するMenuBookクラスについて説明します。
void Menu::setName(const string& newName){
this->name = newName;
}
string Menu::getName(void){
return this->name;
}
###③MenuBookクラス
MenuBookはMenuクラスをvectorで管理するクラスです。Iteratorを実装する集合体なのでAggregateを継承しています。(集合体を管理するクラスにイテレーター生成するメソッドを持たせるのは自然ですね)
class MenuBook:public Aggregate{
private:
vector<Menu> book;//Menuを管理するvector
unsigned int last = 0;//vectorが管理するメニューの総数
public:
Menu getBookAt(int index);//指定のメニューを取得する
void appendBook(const Menu& newMenu);//メニューブックにメニューを追加する
size_t getLength(void);//メニューブックに登録されているメニュー数を返す
Iterator* myIterator(void);//MenuBookのイテレーターとしてMenuBookIteratorを生成し返す
~MenuBook(){/*cout<<"MenuBookは解体された"<<endl;*/}
};
各メソッドの実装は下記の通りです。メソッドは指定のメニューを取得したり、メニューブックにメニューを追加したり、メニューの数を返したりするものが主です。要はメニューブックの管理をするためのメソッドがてんこ盛りなクラスです。ポイントはIterator *p = new MenuBookIterator(this)のところです。ここで、MenuBookIteratorクラスに自身を指すポインタ(this)を渡すことで、自身をIteratorで管理できるようにしています。
//指定のメニューを取得する
Menu MenuBook::getBookAt(int index){
return book[index];
}
//メニューブックにメニューを追加する
void MenuBook::appendBook(const Menu& newMenu){
book.push_back(newMenu);
last++;
}
//メニューブックに登録されているメニュー数を返す
size_t MenuBook::getLength(void){
return book.size();
}
//MenuBookのイテレータであるMenuBookIteratorを生成して返す
Iterator* MenuBook::myIterator(){
//ポインタを受け取った時の、MenuBookIteratorが定義されていないと
//No matching constructor for initialization of 'MenuBookIterator'
Iterator *p = new MenuBookIterator(this);
return p;
}
###④MenuBookIteratorクラス
Iteratorインターフェースが提供する機能を実装するクラスです。名前の通り、MenuBookクラス専用のIteratorで、Itertatorインターフェースを継承しています。。実装は下記の通りです。MenuBookクラスのメンバを持っています。これは、そもそもMenuBookの構成を知っていないとIteratorも実装しようがないからですね。indexがイテレータがMenuBookのどこを指し示しているかを記録しています。
このクラスでポインタを受け取った時のコンストラクタを明示的に定義して置かないと、MenuBookクラスのmyIterator()メソッドがエラーになるので注意してください。(基本的にポインタを受け取った時のコンストラクタは暗黙的に定義されていないことが大半なので、自分で定義する必要があります。)
class MenuBookIterator:public Iterator{
private:
MenuBook menubook;//メニューブック
unsigned int index;//メニューブックのどこをイテレータが指しているかを記録する変数
public:
//コンストラクタ
MenuBookIterator(MenuBook newMenubook){
this->menubook = newMenubook;
this->index = 0;
}
//ポインタを受け取った時のコンストラクタ
//これがないとIterator *p = new MenuBookIterator(this);でエラー
explicit MenuBookIterator(MenuBook *newMenubook){
this->menubook = *newMenubook;
this->index = 0;
}
//デストラクタ
~MenuBookIterator(){/*cout<<"MenuBookIteratorは解体された"<<endl;*/}
bool hasNext(void);
Menu next(void);
};
各メソッドの実装は下記の通りです。hasNext()メソッドは現在イテレータが指し示すメニューブックに次のメニューが存在するか返すメソッドです。このメソッドがfalseを返してきたらそのメニューブックのスキャンが終了したということです。nextメソッドはイテレータが現在、指し示すメニューを返します。イテレータの目的はメニューブックの順番にスキャンすることなのでinedxを1進めることを忘れないようにしましょう。
//イテレータが現在指し示す要素の次が存在するか指し示している。
bool MenuBookIterator::hasNext(void){
if(index < menubook.getLength()){
return true;
}
else{
return false;
}
}
//イテレータが現在、指し示すメニューを返して、idexを一進める。
Menu MenuBookIterator::next(void){
Menu menu = menubook.getBookAt(index);
index ++;
return menu;
}
#イテレータを用いたサンプル③ 実際に使ってみる
main関数の中で,イテレータを実際に使ってみました。
int main(int argc, const char * argv[]) {
//Menuオブジェクトを生成
Menu menu1("Omurice");
Menu menu2("CurryRice");
Menu menu3("BlackRice");
Menu menu4("WhiteRIce");
//生成したMenuをMenuBookにメニューを追加
MenuBook menubook;
menubook.appendBook(menu1);
menubook.appendBook(menu2);
menubook.appendBook(menu3);
menubook.appendBook(menu4);
//MenuBookのiteratorを生成
//Iertatorはmenubook自身を受け取る
Iterator* it = menubook.myIterator();
while(it->hasNext()){
//it->next()はmenubookの登録されたmenuを指す。
cout << it->next().getName() <<endl;//イテレータが指すメニューの名前を表示
}
delete(it);
return 0;
}
実行結果、イテレータが正しく機能してますね。
Omurice
CurryRice
BlackRice
WhiteRIce
#イテレータの優れている点
イテレータパターンの優れている点は将来的に、MenuBookの管理の仕方が変わってもスキャンに関する処理の変更が少なくて済むという点です。
例えば、今回のMenuBookクラスでは各メニューをvectorで管理していますが、将来これを配列や別のコンテナに変えて管理することになったとします。イテレータがない場合は、main関数内のスキャン処理を各メニューの管理が配列だったら配列のためのスキャン処理に別のコンテナだったらそのコンテナ特有のスキャン処理に書き直す必要が出てきます。main関数内に存在するスキャン処理が1つや2つだったら手間ではないかもしれませんが、数十、数百とスキャン処理があったら発狂していまいますよね。こんな時に、各メニュー管理の方法がどうであれ、Main関数では同じ記述でスキャン処理ができるような設計方法があったらこれほど楽なことはありませんよね。この楽してスキャンしたいというニーズに応えたのがイテレータパターンという訳です。(イテレータをメニューの管理方法に合わせて書き直す手間は当然必要です。ですが、main関数のスキャン処理全て書き直すことに比べたら断然、イテレータだけを書き直した方が楽ですよね)
逆に言えば、イテレータパターンを応用して設計したつもりでも、main関数で記述したスキャン処理を大幅に書き換える必要がある場合は何の意味もないという訳ですね。
#組み込みエンジニアとしてデザインパターンとイテレータについて思うこと
正直、イテレータは組み込みではそんなに使う機会はないなと思いました。組み込みにおいて、コンテナや配列に逐次、要素を追加したり順番に表示したりする機会がそもそもあまりありませんし、クラスが増える分、メモリを圧迫することやソフトウェアのサイズが増大する懸念もあります。(アプデが大変になったり、CPU使用率が上がったりもすると思います)。しかしながら、ソフトウェアの再利用性を高めてくれる、デザインパターンを学ぶ価値は確かにあると感じます。オブジェクト指向について学べて、C++の練習にもなるし一石二鳥です。
#まとめ
デザインパターン自体が形骸化しているという意見ももちろんあります。実際、自分でイテレータを実装しなくてもC++の大体のコンテナには標準装備されているので実装する機会も少ないかもしれません。ですが、デザインパターンを学ばないとイテレータの存在を知らないまま終わるでしょうし、その利点もわからないと思うので古典として学ぶ価値は十分にあると感じました。