switch文をオブジェクト指向らしいコードにリファクタリングしたい

[戻る]
ソイレントグリーン 2007/12/29(土) 09:07:00
お世話になります、ある条件による、一意の処理を行う場合
switch ( n ) {
  case x:
    処理〜
    break:
  case y:
    処理〜
    break:
  以下、条件の数だけ同様に続く
  }

nの条件で処理を分岐させる場合、C++初学者において、この様な書き方は、良くあると思
いますし、間違いではないと思うのですが、デザインパターンを学習していくと、「複数
のswitch文が連続する場合、Polymorphismを一考するべし」という事を学びました。ま
た、最近、複数のswitch文は、およそC++らしくない感情を抱いています、自分の書いた
古いコードもなんとか、オブジェクト指向を意識したコードに、リファクタリングできな
いものかと考えるようになりました。

そしてこの様な問題に、ぶつかりました。
下記のようなStateパターンの実装に、本来の基底クラスにメンバ関数を追加たら
error C2259: 'StatePtn' : 抽象クラスをインスタンス化できません。
次のメンバが原因です: 'void StatePtn::Eat(void)' : は抽象型です
とエラーになります。

この様なケースの場合、以前書いたコードをリファクタリングを行いたくても、
基底クラスを純仮想関数に書き直し、メンバ関数は全て、派生関数にしなければならなく
なり、それはそれで、十分慎重に行うべきもので、既にリファクタリングの域を越え、作
り直しのようなものに映ります。

実装は全てデザインパターンに当てはめるべきなどとは、毛頭考えていませんが、
この様に基底クラスのメンバ関数と、純仮想関数から派生した関数を住み分けさせたい場
合、どの様にするのが、オブジェクト指向らしい実装なのでしょうか?

#include <stdio.h>  // puts()
#include <stdlib.h> // rand()

// Stateパターン

class StatePtn
{
public:
  virtual void  Eat() = 0 ; // 食べる(共通動作)
  void Drink()    // 飲む(このメンバ関数を追加したのですがerror C2259:)
  {
    puts("ジュース大好き!。ゴクゴク。");
  }
} ;

// 実装

class Apple : public StatePtn
{
public:
    virtual void  Eat()
    {
        puts( "リンゴ大好き!。パクパク。" ) ;
    }
} ;

class Banana : public StatePtn
{
public:
    virtual void  Eat()
    {
        puts( "バナナおいしいね。ムシャムシャ。" ) ;
    }
} ;

class Hirosue
{
    StatePtn *  m_stat ; // 状態、1つしか持たない。

public:
    Hirosue()
    : m_stat( 0 )
    {}

public:
    void  Have( StatePtn * in )
    {
        m_stat = in ;
    }

public:
    void  DoSomething()
    {
        // なにかをする。(状態によってなにをするのかが変わる)
        if ( m_stat != 0 )  m_stat->Eat() ; // 持っているモノを食べる
        else puts( "なにも持ってませーん。" ) ;
    }
} ;

int main()
{
    Apple   apple ;
    Banana  banana ;

    Hirosue  ryoko ;
    StatePtn sp;  //基底クラスを呼出したいので追加したが、error C2259:

    for ( int i=0 ; i<10 ; ++i )
    {
        // 乱数で行動を決定
        int r = rand() % 4 ;

        printf( "%d : %d ", i, r ) ;
        switch ( r )
        {
        case 0 :
            ryoko.Have( & apple ) ;
            puts( "リンゴをもらいました。" ) ;
            break ;
        case 1 :
            ryoko.Have( & banana ) ;
            puts( "バナナをもらいました。" ) ;
            break ;
        case 2 :
            ryoko.DoSomething() ; // なにかをする
            break ;
        default:
            puts( "ひまですぅ" ) ; // なにもしない
            break ;
        }
        // end switch r
    }
    // end for i

    return  0 ;
}

Ban 2007/12/29(土) 10:09:59
# リファクタリングするなら、変更結果が問題ないことを保証/確認するための、
# UnitTestコードを先に書くべきだと思いますが。
# そもそも、リファクタリングってもともと「動いているものの書き直し」ですから、
# 全部慎重にやるべきですし。(「このくらいは大丈夫」ってのが一番危ない…orz)

なぜ、DrinkをStatePtnに追加したいと思うのか、
さらにそれを直接インスタンス化したいのか、よくわかりませんが、
そう考えてしまう時点で、オブジェクト指向とかデザインパターンとかの
本旨をはずしてたりしませんか?

ただのサンプルであげてるだけかもしれませんが、Eatを持つ基底クラスの名前が、
StatePtnっていうのが何をしたいのかよくわからなくなってる一因かも。

EatとDrinkを同列に扱いたいなら、Eatじゃなくて、
DoSomethingならDoSomethingのStateを持てばよいのでは?

Eatとは無関係のDrinkを持ちたいなら、それが本当にStatePtnが持つべきなのか?
持つべきであるなら、StatePtnを派生させればよいのではないか?
(インターフェイスの)多重継承ではいけないのか?

# StatePtnのありがちな使い方としては、NullObjectパターンと組み合わせて
# 「食べない」という選択肢もStatePtnのサブクラスにする形だと思いますが…。

Ban 2007/12/29(土) 10:26:14
# コンパイルもしてない適当なものですが、ちょっといじってみました。
# 「デザインパターン」とかいうものは状況にあわせて選択するもので、
# 背景情報がないのでなんとも…という部分と、
# 自分でゼロから書くならこうはしないなぁ…という部分もありますが。

// 手抜き。NullObjectと兼用(StatePtnとは別にDoNothingを作る選択肢もある)
// この例では、EatとDrinkはEatとDrinkは区別される必要があるもの。
// 区別しないなら、EatとDrinkを分ける意味はない。
class StatePtn
{
public:
  virtual void  Eat()
  {
    puts( "食べ物を持ってませーん。" ) ;
  }
  virtual void Drink()
  {
    puts( "飲み物を持ってませーん。" ) ;
  }
  virtual ~StatePtn(){} // とりあえずお約束
} ;

// 実装

class Apple : public StatePtn
{
public:
    virtual void  Eat()
    {
        puts( "リンゴ大好き!。パクパク。" ) ;
    }
    virtual void  Drink()
    {
        puts( "見るがよい!私の握力を持ってすれば林檎ジュースを作るなど造作もな
いことっ!" ) ;
    }
} ;

class Banana : public StatePtn
{
public:
    virtual void  Eat()
    {
        puts( "バナナおいしいね。ムシャムシャ。" ) ;
    }
    virtual void  Drink()
    {
        puts( "バナナジュースはさすがにジューサーで作りたいと思うがどうか…" ) ;
    }
} ;


class Hirosue
{
    StatePtn*  m_stat ; // 状態、1つしか持たない。

public:
    explicit Hirosue(StatePtn* state)
    : m_stat( state )
    {
       if(state == 0) throw std::invalid_argument("state is null.");
    }

public:
    void  Have( StatePtn * in )
    {
        m_stat = in ;
    }

public:
    void  DoSomething()
    {
        m_stat->Eat() ; // 持っているモノを食べる
        m_stat->Eat() ; // 持っているモノを飲む
    }
} ;

int main()
{
    Apple   apple ;
    Banana  banana ;
    StatePtn doNothing;

    Hirosue  ryoko(doNothing);

    ……

Ban 2007/12/29(土) 10:29:04
ぱっと見でうそつき(↓)。飲んでない…orz

>    m_stat->Eat() ; // 持っているモノを飲む

FUKU 2007/12/29(土) 10:45:10
このサンプル、どこかのサイトで見たような気が...^^;
まぁそれは置いといて、

以前私が作ったStateパタンのコードに照らし合わせると
状態基本クラスの各メンバ関数は純粋仮想関数ではなく、通常の仮想関数にしおき

void StatePtn::Eat() {
 puts("今は食べれる状態ではありません");
}
みたいに既定の処理を書いておきます。
で、Contextクラス(上の例ではHirosue)の初期状態はStatePtnのインスタンス
を示すようにしておけば良いでしょう


ところで、
>実装は全てデザインパターンに当てはめるべきなどとは、毛頭考えていませんが、
には同意です。
特にStateパターンは「1状態1クラス」となりコード量が増える上に
可読性は余りよろしく無いように思います(個人的に)。

ソイレントグリーン 2007/12/29(土) 12:35:39
Banさん、FUKUさんお世話になります。
大変有意義なコメントありがとうございます。

>>なぜ、DrinkをStatePtnに追加したいと思うのか、
>>さらにそれを直接インスタンス化したいのか、よくわかりませんが、
>>そう考えてしまう時点で、オブジェクト指向とかデザインパターンとかの
>>本旨をはずしてたりしませんか?
リファクタリングしたい実装コードのswitch()文の部分を、StatePtnに置き換えようと
試みたので、そうなりました先に、実装があったが故です。

>>ただのサンプルであげてるだけかもしれませんが、Eatを持つ基底クラスの名前が、
>>StatePtnっていうのが何をしたいのかよくわからなくなってる一因かも。
下記のサイトがデザインパターンを学ぶのに、分かり易かったのでそのまんま
サンプルを掲示しました。
http://www.01-tec.com/document/cpp_design_pattern.html#State

>>EatとDrinkを同列に扱いたいなら、Eatじゃなくて、
>>DoSomethingならDoSomethingのStateを持てばよいのでは?
なるほど、答えがでました、FUKUさんも、後でご教示してくださいましたが
基底クラスに純仮想関数を持たせず、仮想関数にすることで、出来ますね。

>>Eatとは無関係のDrinkを持ちたいなら、それが本当にStatePtnが持つべきなのか?
>>持つべきであるなら、StatePtnを派生させればよいのではないか?
>>(インターフェイスの)多重継承ではいけないのか?
多重継承は、ソースコードの、見通しが悪くなるのでなるべく使いたくないと・・・

>># StatePtnのありがちな使い方としては、NullObjectパターンと組み合わせて
>># 「食べない」という選択肢もStatePtnのサブクラスにする形だと思いますが…。
「NullObjectパターン」初めて知りました、GOF本の23以外のパターンしか知りませんで
した。

>>このサンプル、どこかのサイトで見たような気が...^^;
...^^;

>>Contextクラス(上の例ではHirosue)の初期状態はStatePtnのインスタンス
>>を示すようにしておけば良いでしょう
Hirosueの部分をContextクラスって言うのですね、インターフェースを行う、クラスと理
解していますが、この様な実装はオブジェクト志向らしくていいですね。(^-^)

>>特にStateパターンは「1状態1クラス」となりコード量が増える上に
>>可読性は余りよろしく無いように思います(個人的に)。
リファクタリングしながら、痛感しました、400行程度の実装を、手直ししているのです
が、
switch()文でブランチしたコードとサイズは殆ど一緒になっています( ゜Д゜)ポカーン
利点として、拡張性が良いとの解説を見かけたこともありますが、副作用がきついです
ね。

結局、switch()文による分岐は、オブジェクト指向らしく、リファクタリングできそうに
ありませんが、新しいパターンといいますか(おそらく、既にあるでしょうが?)
基底クラスに、一括りに出来る、オブジェクトが幾種類かあり、それぞれのオブジェクト
から派生させるメンバ関数を持ち、それらを間接的にインスタンスにしていくパターンと
でも言うのかな?ピラミッド型のねずみ講パターンですねww
実装の引き出しが一つ増えました、どうもありがとうございました。

Ban 2007/12/29(土) 17:27:14
# どこかで見たりんごとバナナだと思ったら「そのまま」でしたか…(素で気づかなかった)

> 多重継承は、ソースコードの、見通しが悪くなるのでなるべく使いたくないと・・・

本当にそうですかね、と私は思いますが。
Javaなどでいうinterfaceとかが、C++では多重継承(の一形態)なわけですが、
Stateパターンで多量なクラスを作るのと比べて、格段に見通しが悪くなるでしょうか。。
Stateもさほど見通しはよくないと思うわけで、もしそうであれば使い方の問題な気がします。
# この例に多重継承使ってうれしいかといわれれば、
# 「100行程度のプログラムはスパゲッティでもかける」と御大の著書にもあるわけですが。

> インターフェースを行う、クラスと理解していますが、

インターフェイスを行うってのはよくわかりませんが、
Stateに対してのContextは、文脈とかモット端的にプログラミング用語でいう
「コンテキスト」とかの事だと思われます。

Ban 2007/12/29(土) 17:30:47
Stateとかいうパターンが役に立つのは、現実と設計と実装の考え方を近づけるOOPの
本質から考えれば、
「多大なイベントと状態を管理する必要がある」
「設計時点で状態遷移表を検証しており、そのままコードに落としたい」
「複数状態の間で、同じ動作/類似の動作/ちょっと例外的な動作などを行うべき状態がある」
などのケースだと思います。

複雑な状態とイベントを持つシステムなどで、状態に応じて特定の場合に特別な動作をしたいとか、
そういう要望を、設計書レベルと近い形で書けるのがStateだと思うわけですが。

各状態間に、ある要素だけ共通部があって、ある部分だけ異なって…
というときなどに、
switch(r)
{
  case WALKING:
  case RUNNING:
     …
     if(r == RUNNING) { … }
     …
     break;
  case STANDING:
     …
などと書くよりは、
class MovingState : class ActionState
{
protected:
   virtual void  _before() { /* nothing to do */ }
   virtual void  _doSomething() { /* nothing to do */ }
   virtual void  _after() { /* nothing to do */ }
public:
   virtual void doSomething()
   {
      _before();
      _doSomething();
      _after(); 
   } 
};

class WalkingState : public MovingState
{
public:
   virtual void  _before() { … }
   virtual void  _doSomething() { … }
   virtual void  _after() { … }
}

class RunningState : public WalkingState 
{
protected:
   virtual void  _doSomething() { … }
};

……
等の方が、多少コードが増えようが、状態の相互関係がわかりやすかったり、
同じ処理を使いまわし易かったりすることがある。
コードと設計書の差異を考えると、ifやswitchにするよりもStateの方がよい、
と判断できるトレードオフが成り立つ、そんな状況が時にある。
switchを置きかたらOOPという考え方は「ありがちな罠」にかかってるような気がします。

ソイレントグリーン 2007/12/30(日) 17:58:12
[[解決]]
Banさん、ソイレントグリーンです、思慮深い解説ありがとうございます
>>複雑な状態とイベントを持つシステムなどで、状態に応じて特定の場合に特別な動作を
>>したいとか、
>>そういう要望を、設計書レベルと近い形で書けるのがStateだと思うわけですが。

switch()文の、分岐先で行う処理が、全て単純な同様の処理の場合、Stateパターンを使
うということは、歩いて5分のコンビに500円の弁当を、買いに行くのに、、運転手付きの
リムジンで行くというような感覚かと理解しました。

>>コードと設計書の差異を考えると、ifやswitchにするよりもStateの方がよい、
>>と判断できるトレードオフが成り立つ、そんな状況が時にある。
>>switchを置きかたらOOPという考え方は「ありがちな罠」にかかってるような気がしま
>>す。

沢山、いろいろなソースコードや本を読んで、経験を積んでいかないと私には、まだ判断
が出来そうもないです。

みなさん、ありがとうございました、解決とさせて頂きます。

古いスレッドには追加発言できません。