カプセル化について

フォーラム(掲示板)ルール
フォーラム(掲示板)ルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
長峰
記事: 32
登録日時: 7年前

カプセル化について

#1

投稿記事 by 長峰 » 7年前

シーケンス遷移の制御をするプログラムを作りたくて、実装方法を色々と参考しました。
それを元に、コードを組んだのですが、随分オープンなコードになってしまいました。
こういったコードを必要な部分だけが見れる様に、カプセル化するにはどうしたらいいでしょうか。

また、そもそもカプセル化とは何なのかが判りません、コードを内部結合にするだけでカプセル化になるのでしょうか。教えてください。

コード:

#include<iostream>
enum SequenceType{ NONE, START_MENU, MAIN_GAME };
SequenceType order=NONE;
void SendOrder(SequenceType order){ ::order = order; };
class Sequence{
public:
	virtual ~Sequence(){}
	virtual void Update() = 0;
	virtual void Draw() = 0;
};
class MainGame:public Sequence{
public:
	MainGame(){}
	~MainGame(){}
	void Update(){}
	void Draw(){ std::cout << "MainGame\n"; }
};
class StartMenu :public Sequence{
	enum MenuItem{ LOAD_GAME,NUM };
	MenuItem SelectedItem;
public:
	StartMenu():SelectedItem(LOAD_GAME){}
	~StartMenu(){}
	void Update()
	{
		char input;
		std::cin >> input;
		switch (input){
		case'w': SelectedItem = static_cast<MenuItem>((SelectedItem + (NUM - 1)) % NUM); break;
		case's': SelectedItem = static_cast<MenuItem>((SelectedItem + 1) % NUM); break;
		case'd':
			switch (SelectedItem){
			case LOAD_GAME:SendOrder(MAIN_GAME); break;
			}
		}
		
	}
	void Draw(){ std::cout << "StartMenu\n"; }
};
//処理ごとにあった分岐のswitch文を1つにまとめる
class PraimaryManager{
	Sequence* seq;//機能の一部をSequenceに委譲
public:
	PraimaryManager() :seq(new StartMenu){ ; }
	~PraimaryManager(){ delete seq; }
	void Update()
	{
		if (order != NONE){
			delete seq;
			switch (order){
			case START_MENU:; seq = new StartMenu; break;
			case MAIN_GAME:; seq = new MainGame; break;
			}
			order = NONE;
		}
		seq->Update();//SendOrderはここでで呼ばれ、1ループ後に上にあるメソッドで処理される。
	}
	void Draw(){ seq->Draw(); };
};
int main(){
	PraimaryManager pm;
	for (int i = 0; i < 10; ++i){
		pm.Update();
		pm.Draw();
	}
}

アバター
softya(ソフト屋)
副管理人
記事: 11677
登録日時: 10年前
住所: 東海地方
連絡を取る:

Re: カプセル化について

#2

投稿記事 by softya(ソフト屋) » 7年前

PraimaryManager以外がorderを書き換えているのでカプセル化しているとは言いがたいですね。
orderを書き換える、参照する、保持するクラスを1つにまとめて外部から直接触れないようにすればカプセル化になります。

【補足】
カプセル化は、オブジェクトの内部の仕様を隠ぺいすることで再利用性と不用意な書き換えによるバグを防ぐのが目的です。
PraimaryManagerのオブジェクトを2つ作った場合、どちらも1つの外部のorderの影響を受けますので利便性は高くないですよね。
これは再利用性が低いことを意味しますので、せっかくのオブジェクト指向が生きてきていません。
by softya(ソフト屋) 方針:私は仕組み・考え方を理解して欲しいので直接的なコードを回答することはまれですので、すぐコードがほしい方はその旨をご明記下さい。私以外の方と交代したいと思います(代わりの方がいる保証は出来かねます)。

長峰
記事: 32
登録日時: 7年前

Re: カプセル化について

#3

投稿記事 by 長峰 » 7年前

softya(ソフト屋)さん回答ありがとうございます。
softya(ソフト屋) さんが書きました:orderを書き換える、参照する、保持するクラスを1つにまとめて外部から直接触れないようにすればカプセル化になります。
それをするとすごい使いにくくなりませんか?
softya(ソフト屋) さんが書きました:カプセル化は、オブジェクトの内部の仕様を隠ぺいすることで再利用性と不用意な書き換えによるバグを防ぐのが目的です。
PraimaryManagerのオブジェクトを2つ作った場合、どちらも1つの外部のorderの影響を受けますので利便性は高くないですよね。
これは再利用性が低いことを意味しますので、せっかくのオブジェクト指向が生きてきていません。
自分としては、PraimaryManagerは2つも作る予定はなかったのですが、
クラスを作るということは、基本的に再利用をするのが前提になっているのでしょうか?
1つしか作らない場合はシングルトンなどを使って明示的にやらないとダメなのですか?

回答して貰ったのに、意図を得ていない返信になってしまってすいません。

アバター
softya(ソフト屋)
副管理人
記事: 11677
登録日時: 10年前
住所: 東海地方
連絡を取る:

Re: カプセル化について

#4

投稿記事 by softya(ソフト屋) » 7年前

オブジェクトを2つ作るって意味じゃなくても、クラスと関数の2つに依存していると分かりづらくなると思うわけです。
別のプロジェクトへの使い回しもややこしくなります。
あと、orderを関数じゃなくクラスにして委譲してPraimaryManagerからSequenceの子クラスに参照で渡したほうが良いじゃないかと思いました。

【補足】
>クラスを作るということは、基本的に再利用をするのが前提になっているのでしょうか?
そのためのクラスだと思います。あとバグが生まれづらくなる工夫も必要かと。ちゃんと書けばバグの抑制には役立ちます。
それじゃなければ、書くのが面倒なクラスなどライブラリだけ使って自分で組まないほうが楽じゃないですか。

とりあえず別の発想の代案を書いてみましたが、ちょっと私的にはアレな感じです。
ただ、stateパターン的にswitchが消えたのでスッキリはしています。
時間ができたら、もう一つぐらい書いてみます。

コード:

#include<iostream>

//	前方宣言
class Sequence;

class PraimaryManager {
	Sequence* seq;//機能の一部をSequenceに委譲
public:
	PraimaryManager();
	~PraimaryManager();
	void Update();
	void Draw();
private:
	void seqUpdate(Sequence* seq);
};

class Sequence {
public:
	virtual ~Sequence() {}
	virtual void Update( PraimaryManager *pCallBackClass,void (PraimaryManager::*seqUpdateCallBack)(Sequence* seq) ) = 0;
	virtual void Draw() = 0;
};

class MainGame: public Sequence {
public:
	MainGame() {}
	~MainGame() {}
	void Update( PraimaryManager *pCallBackClass,void (PraimaryManager::*seqUpdateCallBack)(Sequence* seq) ) {}
	void Draw() {
		std::cout << "MainGame\n";
	}
};

class StartMenu : public Sequence {
	enum MenuItem { LOAD_GAME, NUM };
	MenuItem SelectedItem;
public:
	StartMenu(): SelectedItem( LOAD_GAME ) {}
	~StartMenu() {}
	void Update( PraimaryManager *pCallBackClass,void (PraimaryManager::*seqUpdateCallBack)(Sequence* seq) ) {
		char input;
		std::cin >> input;
		switch ( input ) {
		case'w':
			SelectedItem = static_cast<MenuItem>( ( SelectedItem + ( NUM - 1 ) ) % NUM );
			break;
		case's':
			SelectedItem = static_cast<MenuItem>( ( SelectedItem + 1 ) % NUM );
			break;
		case'd':
			switch ( SelectedItem ) {
			case LOAD_GAME:
				(pCallBackClass->*seqUpdateCallBack)(new MainGame);
				break;
			}
		}

	}
	void Draw() {
		std::cout << "StartMenu\n";
	}
};

//処理ごとにあった分岐のswitch文を1つにまとめる
PraimaryManager::PraimaryManager() : seq( new StartMenu ) {
}
PraimaryManager::~PraimaryManager() {
	delete seq;
}
void PraimaryManager::Update() {
	seq->Update(this,&PraimaryManager::seqUpdate);
}
void PraimaryManager::Draw() {
	seq->Draw();
}
void PraimaryManager::seqUpdate(Sequence* seq) {
	delete this->seq;
	this->seq = seq;
}

int main() {
	PraimaryManager pm;
	for ( int i = 0; i < 10; ++i ) {
		pm.Update();
		pm.Draw();
	}
}
by softya(ソフト屋) 方針:私は仕組み・考え方を理解して欲しいので直接的なコードを回答することはまれですので、すぐコードがほしい方はその旨をご明記下さい。私以外の方と交代したいと思います(代わりの方がいる保証は出来かねます)。

アバター
usao
記事: 1635
登録日時: 7年前

Re: カプセル化について

#5

投稿記事 by usao » 7年前

オフトピック
Sequence::Update()が内部で新しいSequenceをnewで作ってPrimariyManager側に渡す
(deleteはPrimariyManager側が請け負う)という仕組みで良いのならば
Sequence *Sequence::Update();
とかして戻り値で返すのが簡単だと思うのだけど,コールバック指定にしてるのには理由があるのだろうか?

アバター
softya(ソフト屋)
副管理人
記事: 11677
登録日時: 10年前
住所: 東海地方
連絡を取る:

Re: カプセル化について

#6

投稿記事 by softya(ソフト屋) » 7年前

usao さんが書きました:
オフトピック
Sequence::Update()が内部で新しいSequenceをnewで作ってPrimariyManager側に渡す
(deleteはPrimariyManager側が請け負う)という仕組みで良いのならば
Sequence *Sequence::Update();
とかして戻り値で返すのが簡単だと思うのだけど,コールバック指定にしてるのには理由があるのだろうか?
最近Javaやらjavascriptばっかり書いているので、リスナ風にしようとしただけです(汗)。
by softya(ソフト屋) 方針:私は仕組み・考え方を理解して欲しいので直接的なコードを回答することはまれですので、すぐコードがほしい方はその旨をご明記下さい。私以外の方と交代したいと思います(代わりの方がいる保証は出来かねます)。

長峰
記事: 32
登録日時: 7年前

Re: カプセル化について

#7

投稿記事 by 長峰 » 7年前

お二方ともご意見ありがとうございます。
softya(ソフト屋) さんが書きました:オブジェクトを2つ作るって意味じゃなくても、クラスと関数の2つに依存していると分かりづらくなると思うわけです。
別のプロジェクトへの使い回しもややこしくなります。
そうですね。自分も依存関係は最小限に抑えたいと思っています。
softya(ソフト屋) さんが書きました:あと、orderを関数じゃなくクラスにして委譲してPraimaryManagerからSequenceの子クラスに参照で渡したほうが良いじゃないかと思いました。
orderをどこに委譲するのでしょうか?
softya(ソフト屋) さんが書きました: そのためのクラスだと思います。あとバグが生まれづらくなる工夫も必要かと。ちゃんと書けばバグの抑制には役立ちます。
それじゃなければ、書くのが面倒なクラスなどライブラリだけ使って自分で組まないほうが楽じゃないですか。
それを聞くと、自分の規模では、関数としてまとめてしまうくらいで良かったのかもしれません。
大規模になってバグが出ると大変なので、今のうちから気をつけていきます。

書いて貰ったコードは、
変更する時だけ関数(seqUpdate)を呼び出して、
次に遷移するシーケンスを呼びさせば、switch文が要らなくなるのは参考になりました。

しかし、私の理解が及ばないため
Sequenceのメンバ関数Updateが仮引数のvoid型の所が分かりません。
キャストを行っているのでしょうか?
関数seqUpdateの参照を受け取っているのはわかるのですが。

アバター
softya(ソフト屋)
副管理人
記事: 11677
登録日時: 10年前
住所: 東海地方
連絡を取る:

Re: カプセル化について

#8

投稿記事 by softya(ソフト屋) » 7年前

> orderをどこに委譲するのでしょうか?

orderをクラス化してPraimaryManagerに委譲した機能として組み込むという話です。

>Sequenceのメンバ関数Updateが仮引数のvoid型の所が分かりません。

void (PraimaryManager::*seqUpdateCallBack)(Sequence* seq)
の事でしょうか?
voidは戻り値の型です。メンバ関数のポインタですので型として戻り値の型も必要です。
by softya(ソフト屋) 方針:私は仕組み・考え方を理解して欲しいので直接的なコードを回答することはまれですので、すぐコードがほしい方はその旨をご明記下さい。私以外の方と交代したいと思います(代わりの方がいる保証は出来かねます)。

アバター
softya(ソフト屋)
副管理人
記事: 11677
登録日時: 10年前
住所: 東海地方
連絡を取る:

Re: カプセル化について

#9

投稿記事 by softya(ソフト屋) » 7年前

参考例として上げておきます。

コード:

#include<iostream>

//	前方宣言
class Sequence;

class SequenceOrder {
	Sequence* seq;
public:
	SequenceOrder() {
		seq = NULL;
	};
	~SequenceOrder() {
		if( seq!=NULL ) {
			delete seq;
		}
	};
	void setSeq(Sequence* seq) {
		if( seq!=NULL ) {
			delete this->seq;
		}
		this->seq = seq;
	}
	Sequence* getSeq() {
		return this->seq;
	}
};

class Sequence {
public:
	virtual ~Sequence() {}
	virtual void Update( SequenceOrder &seqOrder ) = 0;
	virtual void Draw() = 0;
};

class MainGame: public Sequence {
public:
	MainGame() {}
	~MainGame() {}
	void Update( SequenceOrder &seqOrder ) {}
	void Draw() {
		std::cout << "MainGame\n";
	}
};

class StartMenu : public Sequence {
	enum MenuItem { LOAD_GAME, NUM };
	MenuItem SelectedItem;
public:
	StartMenu(): SelectedItem( LOAD_GAME ) {}
	~StartMenu() {}
	void Update( SequenceOrder &seqOrder) {
		char input;
		std::cin >> input;
		switch ( input ) {
		case'w':
			SelectedItem = static_cast<MenuItem>( ( SelectedItem + ( NUM - 1 ) ) % NUM );
			break;
		case's':
			SelectedItem = static_cast<MenuItem>( ( SelectedItem + 1 ) % NUM );
			break;
		case'd':
			switch ( SelectedItem ) {
			case LOAD_GAME:
				seqOrder.setSeq(new MainGame);
				break;
			}
		}

	}
	void Draw() {
		std::cout << "StartMenu\n";
	}
};

class PraimaryManager {
	SequenceOrder seqOrder;//機能の一部をSequenceOrderに委譲
public:
	PraimaryManager() {
		seqOrder.setSeq(new StartMenu);
	};
	~PraimaryManager() {
	};
	void Update() {
		Sequence* seq = seqOrder.getSeq();
		if( seq!=NULL ) {
			seq->Update(seqOrder);
		}
	}
	void Draw() {
		Sequence* seq = seqOrder.getSeq();
		if( seq!=NULL ) {
			seq->Draw();
		}
	}
};

int main() {
	PraimaryManager pm;
	for ( int i = 0; i < 10; ++i ) {
		pm.Update();
		pm.Draw();
	}
}
by softya(ソフト屋) 方針:私は仕組み・考え方を理解して欲しいので直接的なコードを回答することはまれですので、すぐコードがほしい方はその旨をご明記下さい。私以外の方と交代したいと思います(代わりの方がいる保証は出来かねます)。

長峰
記事: 32
登録日時: 7年前

Re: カプセル化について

#10

投稿記事 by 長峰 » 7年前

返信が遅くなってごめんなさい。
softya(ソフト屋) さんが書きました:voidは戻り値の型です。メンバ関数のポインタですので型として戻り値の型も必要です。
関数のポンタの渡し方を知りませんでした。
勘違いが解けて良かったです。

2つ目のコードを参考しました。
SequenceOrderをクラス化するとかなり便利になりますね。
クラスの再利用性も高そうなので、Sequenceの階層を簡単に作れそうです。

コードの整理はまだついていませんが、
どのようにクラスを作ってカプセル化を行うか、見当がつきましたので本トピックを解決とさせていただきます。

意見をくれたusaoさん、
コードを2つ書いて下さったsoftyaさん、大変ありがとうございました。

閉鎖

“C言語何でも質問掲示板” へ戻る