概要
WindowsのGUIアプリでモードによって処理を切り替えたいとき、Stateパターンを用いることで分岐処理を省略することができました。
知識しかなかったデザインパターンの有効性を強く感じて、嬉しくなったので記事にしています。
良くなったと思う点
1. 動作が増えたときにSwitch文を書く必要がなく、ソースコードの量が減る
2. モードが増えたときにすべてのSwitch分に追加する必要がなく、Factoryに追加するだけで済む
3. 共通処理を継承元に置くことで、switchの分岐をまとめるよりも共通化が行いやすい
改善したいコード
例として、クリックした点を使用した図形描画を考えます。
点、三角形、四角形などは必要な点数が異なるため、クリックされたときに単純に分岐を書くとパターンが膨大になってしまいます。
また、行いたい動作事に分岐が必要になりソースコードの量が増え、モードを追加したときの変更箇所が増えてしまいます。
#pragma region UI部分
class BadUIManager
{
public:
enum eMode
{
POINT,
TRIANGLE,
RECT
};
void ChangeMode(int listIdx);
void OperationClick(CPoint pt);
void OperationMouseMove(CPoint pt);
void OperationBack();
void OperationRestart();
private:
eMode _mode;
strConfig _config;
vector<CPoint> _pts;
};
//モード変更したとき
inline void BadUIManager::ChangeMode(int listIdx)
{
_mode = static_cast<eMode>(listIdx);
}
//クリックされたとき
inline void BadUIManager::OperationClick(CPoint pt)
{
_pts.push_back(pt);
switch (_mode)
{
case BadUIManager::POINT:
//点の描画処理
break;
case BadUIManager::TRIANGLE:
if (_pts.size() == 3) {
//三角形の描画登録処理
_pts.clear();
}
break;
case BadUIManager::RECT:
if (_pts.size() == 4) {
//四角形の描画登録処理
_pts.clear();
}
break;
default:
break;
}
}
//マウスが動いたとき
inline void BadUIManager::OperationMouseMove(CPoint pt)
{
switch (_mode)
{
case BadUIManager::TRIANGLE:
//三角形の仮描画処理
break;
case BadUIManager::RECT:
//四角形の仮描画処理
break;
case BadUIManager::POINT:
default:
//点モードでは仮描画を行わない
break;
}
}
//戻るを押したとき
inline void BadUIManager::OperationBack()
{
_pts.pop_back();
//モードによる分岐が必要ならswitchが必要
}
//やり直しのとき
inline void BadUIManager::OperationRestart()
{
//やり直し処理
//モードによる分岐が必要ならswitchが必要
}
#pragma endregion
改善したソースコード
1. インターフェースに各動作(クリック、やり直し、初期化など)をメソッドとして定義します。
2. インターフェースを継承した基底クラスを定義し、共通の動作を記述します。
3. モードによって振る舞いが異なる動作に関してはOverrideすることで異なる処理のみを変更することができます。
4. モードが切り替わった際には、モードに応じたEditorインスタンスを作成するFactoryメソッドをUI側で呼び出します。
クラス図
Editor部分
using namespace std;
//UIから受け取る共通設定
struct strConfig
{
int lineWidth;
COLORREF color;
};
//共通処理のインターフェース
class IShapeEditorInterface
{
public:
virtual void ClickedOperatin(CPoint pt)=0;
virtual void OperationBack()=0;
virtual void OperationRestart()=0;
virtual void MouseMove(CPoint pt) = 0;
};
//図形描画の基底クラス
class ShapeEditorBase:public IShapeEditorInterface
{
public:
enum eMode
{
POINT,
TRIANGLE,
RECT,
};
ShapeEditorBase(strConfig pConfig);
virtual void ClickedOperatin(CPoint pt)override {};
virtual void OperationBack()override;
virtual void OperationRestart()override;
virtual void MouseMove(CPoint pt)override {};
static unique_ptr<ShapeEditorBase> GetShapeEditorFactory(eMode mode, strConfig pConfig);
protected:
strConfig _spConfig;//受け取った共通設定
vector<CPoint> _ClickPts;//クリックされたWindow座標の格納
};
#pragma region 全図形の共通処理
//
ShapeEditorBase::ShapeEditorBase(strConfig pConfig)
{
_spConfig = pConfig;//共通のオプションを受け取る
}
inline void ShapeEditorBase::OperationBack()
{
_ClickPts.pop_back();
}
inline void ShapeEditorBase::OperationRestart()
{
_ClickPts.clear();
}
#pragma endregion //全図形の共通処理
#pragma region 点描画
class ShapePointEdtor:public ShapeEditorBase
{
public:
ShapePointEdtor(strConfig pConfig);
private:
virtual void ClickedOperatin(CPoint pt)override ;
};
ShapePointEdtor::ShapePointEdtor(strConfig pConfig):ShapeEditorBase(pConfig)
{
}
inline void ShapePointEdtor::ClickedOperatin(CPoint pt)
{
_ClickPts.push_back(pt);
//点の場合はすぐに描画する
}
#pragma endregion
#pragma region 三角形描画
class ShapeTriangleEditor:public ShapeEditorBase
{
public:
ShapeTriangleEditor(strConfig pConfig);
virtual void ClickedOperatin(CPoint pt)override;
virtual void MouseMove(CPoint pt)override ;
private:
};
ShapeTriangleEditor::ShapeTriangleEditor(strConfig pConfig) :ShapeEditorBase(pConfig)
{
}
inline void ShapeTriangleEditor::ClickedOperatin(CPoint pt)
{
_ClickPts.push_back(pt);
if (_ClickPts.size() == 3) {
//描画と登録処理を行う
_ClickPts.clear();
}
}
inline void ShapeTriangleEditor::MouseMove(CPoint pt)
{
if (_ClickPts.size() < 1)
return;
//2点目からは仮描画を行う
//仮描画処理
}
#pragma endregion
#pragma region 四角形
class ShapeRectEditor:public ShapeEditorBase
{
public:
ShapeRectEditor(strConfig pConfig);
virtual void ClickedOperatin(CPoint pt)override;
virtual void MouseMove(CPoint pt)override;
private:
};
ShapeRectEditor::ShapeRectEditor(strConfig pConfig) :ShapeEditorBase(pConfig)
{
}
inline void ShapeRectEditor::ClickedOperatin(CPoint pt)
{
_ClickPts.push_back(pt);
if (_ClickPts.size() == 4) {
//描画と登録処理を行う
_ClickPts.clear();
}
}
inline void ShapeRectEditor::MouseMove(CPoint pt)
{
if (_ClickPts.size() < 1)
return;
//2点目からは仮描画を行う
//仮描画処理
}
#pragma endregion
inline unique_ptr<ShapeEditorBase> ShapeEditorBase::GetShapeEditorFactory(eMode mode, strConfig pConfig)
{
switch (mode)
{
case ShapeEditorBase::POINT:
return make_unique<ShapePointEdtor>(pConfig);
case ShapeEditorBase::TRIANGLE:
return make_unique<ShapePointEdtor>(pConfig);
case ShapeEditorBase::RECT:
return make_unique<ShapePointEdtor>(pConfig);
default:
return nullptr;//ここで例外処理を行うのも良し
}
}
UI部分
UI部分のリストボックスやボタンでモード切替ややり直しを行うことを想定しています。
処理をEditorクラスに委譲することで分岐処理がなくなり、コードが見やすくなったのではないでしょうか。
#pragma region UI部分
class UIManager
{
public:
void ChangeMode(int listIdx);
void OperationClick(CPoint pt);
void OperationMouseMove(CPoint pt);
void OperationBack();
void OperationRestart();
private:
unique_ptr<IShapeEditorInterface> _upShapeEditor;
strConfig _config;
};
//モード変更したとき
inline void UIManager::ChangeMode(int listIdx)
{
if (_upShapeEditor)
_upShapeEditor.reset();
_upShapeEditor = ShapeEditorBase::GetShapeEditorFactory(static_cast<ShapeEditorBase::eMode>(listIdx), _config);
}
//クリックされたとき
inline void UIManager::OperationClick(CPoint pt)
{
_upShapeEditor->ClickedOperatin(pt);
}
//マウスが動いたとき
inline void UIManager::OperationMouseMove(CPoint pt)
{
_upShapeEditor->MouseMove(pt);
}
//戻るを押したとき
inline void UIManager::OperationBack()
{
_upShapeEditor->OperationBack();
}
//やり直しのとき
inline void UIManager::OperationRestart()
{
_upShapeEditor->OperationRestart();
}
#pragma endregion