BASIC OOP ~Vol.2~

Template Method
概要
- 大枠のメソッド定義を行い、サブクラスで実装する
解説
ここでは「ログファイルの出力」という要求仕様があり
「マルチプラットフォーム運用」を視野に入れたと仮定します。
まず設計を始める際に考えるべきなのは
- 利用者が行う命令の種類
- 命令の実行内容
の2種類に要求仕様を選別してしまいましょう。
大雑把でもイメージが固まれば良いかと思います。
選別がある程度出来たら
- 「利用者が行う命令の種類」=「スーパークラスが備えるメソッド」
- 「命令の実行内容」=「サブクラスの実装内容」
として具体的な実装内容を考えてみます。
冒頭の例ですと
- 「利用者が行う命令の種類」=「ログファイルの出力」=「スーパークラスに実装」
- 「命令の実行内容」=「任意のプラットフォームでログファイルの出力」=「サブクラスに実装」
というイメージが出来ていれば問題ないでしょう。
「ログファイルの出力」を実現するには
- オープン
- ライト
- クローズ
のファイル操作が最低限出来れば良さそうです。
最初から風呂敷を広げ過ぎないのも重要だったりします…。
一先ず最低限の機能を備えたスーパークラスの定義を行いました。
// ログファイル出力スーパークラス class ILogFile { protected: ILogFile() = default; public: virtual ~ILogFile() = default; public: // オープン virtual bool Open(const char* path) = 0; // ライト virtual bool Write(const char* str) = 0; // クローズ virtual void Close() = 0; };
後はプラットフォーム毎に「命令の実行内容」
を実装したサブクラスを定義していきます。
サンプルなのでプラットフォームはどちらもWindowsなのですが
実装内容が異なる2つのサブクラスを定義しました。
// ログファイル出力クラス(FILE使用) class CLogFile final : public ILogFile { public: CLogFile() : ILogFile(), m_pFile(nullptr) {} virtual ~CLogFile() { Close(); } public: // オープン virtual bool Open(const char* path) { Close(); return ::fopen_s(&m_pFile, path, "w") != 0; } // ライト virtual bool Write(const char* str) { if (m_pFile) { const int len = sizeof(char) * (strlen(str)); return std::fwrite(str, len, 1, m_pFile) == len; } return false; } // クローズ virtual void Close() { if (m_pFile) { std::fclose(m_pFile); m_pFile = nullptr; } } private: std::FILE* m_pFile; };
// ログファイル出力クラス(ofstream使用) class CLogStream final : public ILogFile { public: CLogStream() : ILogFile(), m_ofStream() {} virtual ~CLogStream() { Close(); } public: // オープン virtual bool Open(const char* path) { Close(); m_ofStream.open(path, std::ios_base::out); return !m_ofStream.bad(); } // ライト virtual bool Write(const char* str) { m_ofStream.write(str, sizeof(char) * (strlen(str))); return !m_ofStream.bad(); } // クローズ virtual void Close() { m_ofStream.close(); } private: std::ofstream m_ofStream; };
これらを実際に利用してみます。
int main() { ILogFile* p = nullptr; // { // FILE使用版 CLogFile file; p = &file; // 同じ命令 p->Open("sample0.txt"); p->Write("test0"); p->Close(); } { // ofstream使用版 CLogStream stream; p = &stream; // 同じ命令 p->Open("sample1.txt"); p->Write("test1"); p->Close(); } return EXIT_SUCCESS; }
実行結果
(sample0.txt) test0 (sample1.txt) test1
利用者は対応プラットフォームのサブクラスを切り替えていますが
同じ命令で異なる結果が得ることが出来ます。
(サンプルはどちらもWindowsで同じなので注意)
この「同じ命令で異なる結果を得る」という思想は
OOPにおいて非常に重要なので常に意識して欲しいです。
以下はデメリットがあるケースのサンプルになります。
// スーパークラス class ISample { protected: ISample() = default; public: virtual ~ISample() = default; public: // 初期化 virtual bool Initialize() = 0; };
// スーパークラス class CSample : public ISample { protected: CSample() : ISample(), m_iSeed(0) {} public: virtual ~CSample() = default; public: // 初期化 virtual bool Initialize() { // 乱数でシードを生成 m_iSeed = std::rand(); return true; } public: // シードの取得 int GetSeed() const { return m_iSeed; } private: int m_iSeed; };
// サブクラス class CSampleSub final : public CSample { public: CSampleSub() : CSample() {} virtual ~CSampleSub() = default; public: // 初期化 virtual bool Initialize() { // CSample::Initialize呼び出し前なのでここでは0 // 設定済みだと想定しているなら想定外 int s = GetSeed(); // 呼び出しは必須? // スーパークラスの実装を見ないといけない return CSample::Initialize(); } };
スーパークラスで仮想関数が実装されている場合においては
仮想関数の数に比例してサブクラス実装時の解析コストが高まります。
回避方法と言われると難しいのですが
- インタフェースの役割を明確化する
- インタフェースの巨大化を防止する
といった対応で影響を多少緩和することが出来るでしょう。
抽象化もやりすぎると逆効果になりますので注意が必要です。