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

switch文をオブジェクト指向らしい...
 
通知
すべてクリア

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


ソイレントグリーン
 ソイレントグリーン
(@ソイレントグリーン)
ゲスト
結合: 17年前
投稿: 65
Topic starter  

お世話になります、ある条件による、一意の処理を行う場合
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
 Ban
(@ban)
Prominent Member
結合: 5年前
投稿: 776
 

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

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

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

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

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

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


返信引用
Ban
 Ban
(@ban)
Prominent Member
結合: 5年前
投稿: 776
 

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

// 手抜き。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
 Ban
(@ban)
Prominent Member
結合: 5年前
投稿: 776
 

ぱっと見でうそつき(↓)。飲んでない…orz

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


返信引用
FUKU
 FUKU
(@FUKU)
ゲスト
結合: 17年前
投稿: 73
 

このサンプル、どこかのサイトで見たような気が...^^;
まぁそれは置いといて、

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

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

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


返信引用
ソイレントグリーン
 ソイレントグリーン
(@ソイレントグリーン)
ゲスト
結合: 17年前
投稿: 65
Topic starter  

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
 Ban
(@ban)
Prominent Member
結合: 5年前
投稿: 776
 

# どこかで見たりんごとバナナだと思ったら「そのまま」でしたか…(素で気づかなかった)

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

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

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

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


返信引用
Ban
 Ban
(@ban)
Prominent Member
結合: 5年前
投稿: 776
 

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という考え方は「ありがちな罠」にかかってるような気がします。


返信引用
ソイレントグリーン
 ソイレントグリーン
(@ソイレントグリーン)
ゲスト
結合: 17年前
投稿: 65
Topic starter  

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

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

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

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

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


返信引用

返信する

投稿者名

投稿者メールアドレス

タイトル *

プレビュー 0リビジョン 保存しました
共有:
タイトルとURLをコピーしました