まずはじめに
ふと思ったことで良くある話しだと思う。あるオブジェクトがあったとき、そのメソッドの振る舞いをダイナミックに変更したいなら、どのように設計し実装するべきかということ。例えば、処理A-B-Cをするクラスがあったら、A-B、A-C、A-B-C、のように動作変えるにはどうするかということ。結局ストラテジパターン、状況によってはフラグ管理を使うと良いよという話し。
どういう方法があるのかと、その例
とりあえず選択肢はいくつかあると思う。
- ストラテジパターンを使う。(参考)
- クラスにStaticなフラグ変数を用意する(一般敵には良くないと言われている)
- クラスA(A-B-C処理)を継承して、異なったクラスB(A-B処理)、クラスC(A-C処理)を作る
自分が考えつくのはこれぐらいかなぁ。で、それぞれを考えてみる。
ストラテジパターンの場合
まず、ストラテジパターンの場合。例えばこんな感じになるかな。
//メソッド入れ替え用のインターフェース class ForAMethod{ public: //継承メソッドの定義用 typedef int RetT; //メソッド返値 typedef int Arg1T; //第1引数 //オーバーライド用の純粋仮想関数 virtual RetT f(Arg1T x) = 0; }; //メソッドAクラス class MethodA : public ForAMethod{ ForAMethod::RetT f(Arg1T x){ std::cout << x << std::endl; return 0; } }; //メソッドBクラス class MethodB : public ForAMethod{ ForAMethod::RetT f(Arg1T x){ std::cout << x*2 << std::endl; return 0; } }; //コアクラス class A{ public: boost::shared_ptr<ForAMethod> f;//ストラテジパターン用変数 int x; A(); //ストラテジ設定 set_method(boost::shared_ptr<ForAMethod> f){ this->f = f; } //メソッド実行 ForAMethod::RetT proc(){ return f->f(x); } }; int main(){ A obj; boost::shared_ptr<ForAMethod> fm(new MethodB()); obj.set_method(fm); //設定するクラスインスタンスでメソッドの切り替え obj.proc(); }
※コンパイルしてない適当なコードなので、雰囲気で感じてください。
ストラテジパターンはでまず、異なる処理内容ごとにクラスを作ります。そしてそのインスタンスをコアのクラスに設定することで、コアクラスでの処理内容を変えるという方法。これには次のメリットデメリットがあると思う。
デメリット
- デザインパターン全般に言えるが、(デザインパターンを理解していないと第三者から)構造が複雑になりコード量が増える
- 事前にストラテジパターン適用メソッドを選定する必要がある
- ストラテジオブジェクトを受け取るクラスの生成が煩雑になる
メリット
- コード内でメソッドが追加 & 入れ替わるような時に強い
フラグで動作を変更する場合
次の、フラグ管理の場合。この場合は次のようなコードになるかな。
class A{ public: static unsigned int flg = 0x0; //フラグ変数 //フラグ定数 const static unsigned int METHOD_A = 0x01; const static unsigned int METHOD_B = 0x10; int x; A(unsigned int flg){ this->flg |= flg; //フラグ設定 } void proc(){ if(flg & METHOD_A) //メソッドA使用 std::cout << x*2 << std::endl;; else if(flg & METHOD_B) //メソッドB使用 std::cout << x << std::endl; } }; int main(){ A obj(A::METHOD_B); //フラグでメソッドの切り替え obj.proc(); }
これは次のメリットデメリットがあると思う。
デメリット
- コンストラクタが煩雑な引数を取るようになる
- ソースレベルのメソッドの変更に弱い
- メソッドが増える度に、コアのクラスにフラグ定数を追加しないといけない
メリット
- コードが簡単で分かりやすい(古くさいとも)
継承を使った場合
最後の継承の場合。次のようにできるかな。
class A{ public: typedef boost::shared_ptr<A> APtr; int x; A(); virtual int proc(){ std::cout << x << std::endl; //標準処理内容 return 0; } }; //MethodB追加 class A_MethodB : public A{ int proc(){ //関数A::procをオーバーライド std::cout << x*2 << std::endl; return 0; } }; int main(){ boost::shared_ptr<A> obj(new A_methodB()); //インスタンスでメソッドの切り替え obj->proc(); }
メリット
- コード量増分がそこそこで、汎用性もある
- メソッドの追加も容易
デメリット
どれがいいんだろう?
さて、前置きは長くなっちゃったけど、結局どれが良いのか?という話しなのです。
こういうことを考えると、まず『そもそも論』から入らざるを得ない。
考慮すべき挙動変更ポイント
そもそも、挙動を変える原因はどこか?ということを考える。それは次の二通りしかない。
- 実行時オプション
- 実行過程の値
1の実行時オプションはgccを見れば分かるように(あれはもはやオプションがプログラムだと思う)あり得るし、使われるものだ。その理由は2つあるかと思う。それは
- 実行の動作パターンに合わせていちいちコンパイルすると(MethodAを使う実行ファイル、MethodBを使う実行ファイルなど)、コンパイル時間がもったいない
- 動作パターンの組み合わせ数だけ実行ファイルができることにになる
なので、実行時引数で動作を変えると言うのは悪くないというか、やむ得ない。
問題の、2「実行過程の値」だけれど、これは「一旦生成したオブジェクトを使い回しつつメソッドを動的に変更する必要があるか」ということになる。この場合は言い換えると、「同じレベルのデータに対して違うメソッドの結果を組み合わせる必要がある」ということだ。まあ需要がないわけではないと思う。例えばエージェントシステムでの強化学習で、途中から学習ストラテジを変更するとか色々。しかし、基本的には実行時引数である程度決まってしまうものじゃないかなと思う。仮に動的変更(処理AからB)をしたい場合があっても、それに合わせたクラス(処理B)のインスタンスを生成して、それに元のクラス(処理A)の保持データを引き継ぐ、というのでもできる。なので、実行中の値による動的メソッド変更についてはさほど一般的に考慮すべき要素でもないと思う。
保守性で考えるそれぞれの比較
じゃあどの観点でこれら3つの手法を比較すればいいのか?となるけれど、ここはコード保守性をメインに考えてみようと思う。
コード保守性を考える場合、どういった変更がそのソースに加えられるかということになる。
それは次のパターンを考えれば良いと思う。
- メソッドの追加、変更(MethodCが増えるとか)
- 関連クラスの継承(クラスAが継承されたりなど)
一つ一つ考えてみよう。
まずストラテジパターンの場合。これにメソッドを追加するとき3つのファイルを開くか作成する必要がある。それは、
- 新しいメソッドのクラス定義ファイル(.cpp)
- 新しいメソッドのクラス宣言ファイル(.h)
- メソッドクラスインスタンスをコアクラスに渡している所(例:main.cpp)
変更点は、スクラッチから作る定義ファイル(.cpp)と宣言ファイル(.h)。そしてmain.cppの方には、メソッドクラスが変わるので#includeで新しいクラスのヘッダファイルの追加と、メソッドインスタンス生成のクラス変更がいるかな。
例えばこうなるだろうか。
class ForAMethod{ public: typedef int RetT; typedef int Arg1T; virtual RetT f(Arg1T x) = 0; }; class MethodA : public ForAMethod{ ForAMethod::RetT f(Arg1T x){ std::cout << x << std::endl; return 0; } }; class MethodB : public ForAMethod{ ForAMethod::RetT f(Arg1T x){ std::cout << x*2 << std::endl; return 0; } }; //新しく作られたメソッドクラス class MethodB : public ForAMethod{ ForAMethod::RetT f(Arg1T x){ std::cout << x*3 << std::endl; return 0; } }; class A{ public: boost::shared_ptr<ForAMethod> f; int x; A(); set_method(boost::shared_ptr<ForAMethod> f){ this->f = f; } ForAMethod::RetT proc(){ return f->f(x); } }; int main(){ A obj; boost::shared_ptr<ForAMethod> fm(new MethodC()); //変更箇所 obj.set_method(fm); obj.proc(); }
関連クラスの継承後はどうなるかな。
- コアクラスを継承したクラスを作った場合
- 作っても既存のメソッドクラスへの変更は必要ない。またその扱いも変わらない。
- メソッドクラスの継承
- この孫クラスは作るべきではない。仮に作ってもメソッドクラスのベースクラスを継承している以上問題無い。
次にフラグ管理でのメソッド追加。この場合は、3つのファイルを開く必要がある。
- コアクラスファイル(A.cpp, A.h)
- インスタンス生成ファイル(main.cpp)
コアクラスには、動作が変化する部分に分岐を増やし、また、新しいフラグ定数を定義する必要がある。そして、main.cppはフラグを変更する必要がある。例えばこんな
class A{ public: static unsigned int flg = 0x0; const static unsigned int METHOD_A = 0x01; const static unsigned int METHOD_B = 0x10; const static unsigned int METHOD_C = 0x100; //追加箇所 int x; A(unsigned int flg){ this->flg |= flg; //フラグ設定 } void proc(){ if (flg & METHOD_A) std::cout << x*2 << std::endl; else if (flg & METHOD_B) std::cout << x << std::endl; else if (flg & METHOD_C) //追加箇所 std::cout << x << std::endl; //追加箇所 } }; int main(){ A obj(A::METHOD_C); //変更箇所 obj.proc(); }
関連クラスの継承だけれど、そもそもClass Aしか使ってないので問題無い。またフラグ変数はpublicになってるはずなので、新しい継承クラスAAができても、フラグにはAA:METHOD_Bでアクセスできる。
最後の継承の場合のメソッド追加だけれど、この場合も3つのファイルを開く必要がある。
- 新しく継承したクラスの.cppと.h、
- main.cpp
この辺の変更点はほとんどストラテジパターンと同様かな。
class A{ public: typedef boost::shared_ptr<A> APtr; int x; A(); virtual int proc(){ std::cout << x << std::endl; return 0; } }; class A_MethodB : public A{ int proc(){ std::cout << x*2 << std::endl; return 0; } }; //新規作成クラス class A_MethodC : public A{ int proc(){ std::cout << x*3 << std::endl; return 0; } }; int main(){ boost::shared_ptr<A> obj(new A_methodC()); //変更箇所 obj->proc(); }
結論
こう見ると、意外と「フラグ管理法」が優秀に見える。ただ、他と違うのは「既存のライブラリ的なファイル(クラスA)に手を加えないといけない点」だなぁ。つまりこれが何を引き起こすかというと、「ああ、その動作を変えたいの? ならXXX.cppをいじらないとダメだよ」と言うようなやりとりが発生するんじゃないだろうか。保守性とは何か、それは「知らないファイルは極力触らない」とするなら、一般的に言われているようにフラグ分岐は使うべきじゃないとは思った。ただ、次の場合では有効な手法ではあると思う
- ブランチを分けてテスト的なちょっとの変更(Trunkにマージしない)
- 練りに練って、そのクラスにはそれ以上のフラグが存在しない場合
ソースの変更の簡易さはこれらの場合生きるかな
そして残る、委譲(ストラテジパターン)と継承、どっちが良いかということだ。これはコアクラスの管理方法によるかなと。
元々コアクラスをポインタで管理していた場合はどっちもほぼ同じだと思う。ただ、そうでない場合はストラテジパターンがやはり強い。
番外編
残る課題の「そもそも継承を使うべきなのか?」という疑問だけれど、正直使うべきじゃないw。継承とは以下の条件の時に使うべき。それは
- メンバ変数が増える時
この理由は、継承とは、サブクラスをBとしてスーパークラスをAとする。このとき、クラスAのインスタンスの集合をIns(A)、クラスBのインスタンス集合をIns(B)とする。もし、
となるなら、
となる。この辺は型階層論理なんかで説明できる。
で、その場合、
のときに、
とはどういうことかというのが重要になる。それはメンバ変数の比較が全て等しいときだ。したがって、上でいったメソッド追加や変更、上書きのために継承を使うと、
となり、継承を使うのは間違っていることが分かる。
なので、
- テストや一時の変更ではフラグ管理
- そうでない場合はストラテジパターンを使う
ってことかなー。いやーGoFってやっぱ考えられてるなぁと考えてみて実感した