拡張可能なインターフェイス
今回は拡張可能なインターフェイスというものについて考えてみましょう
今 Fig.1 のような抽象基底クラスがあるとします
Fig.1
struct ISamplePlugin : public ISampleUnknown { virtual void* DynamicCast( int classid ) = 0; virtual unsigned long AddRef() = 0; virtual unsigned long Release() = 0; ... virtual bool Save( ISampleSave* pSave ) = 0; virtual bool Load( ISampleLoad* pLoad ) = 0; ... };
このような抽象基底クラスを使ったオブジェクトを実装しようとするユーザーは、自分の実装するオブジェクトにたとえ Save や Load が必要なかろうとも必ず Save も Load も実装する必要があります
Fig.2
class CSamplePlugin : public ISamplePlugin { ... public: ... virtual bool Save( ISampleSave* pSave ); virtual bool Load( ISampleLoad* pLoad ); ... }; bool CSamplePlugin::Save( ISampleSave* pSave ){ return true; } bool CSamplePlugin::Load( ISampleLoad* pLoad ){ return true; }
この例では無駄なメソッドは2つだけですが、それが10も20もあるようだったらどうでしょうか
また Fig.1 の抽象基底クラスに新しく機能を追加しようとするとして Fig.3 のようにした場合はどうでしょうか
Fig.3
struct ISamplePlugin : public ISampleUnknown { virtual void* DynamicCast( int classid ) = 0; virtual unsigned long AddRef() = 0; virtual unsigned long Release() = 0; ... virtual bool Save( ISampleSave* pSave ) = 0; virtual bool Load( ISampleLoad* pLoad ) = 0; ... // 追加機能 virtual int foo() = 0; virtual int bar() = 0; ... };
この追加機能が既存のクラスを実装している人は気にしなくてもいいような機能だったとしても、既存のコードには空の実装を追加することが必要になってしまいます
必要も無いメソッドの実装をいくつも書かなければならないことはユーザーの負担になりますし、既存のコードから気にしなくていい追加機能なら既存のコードは修正しないで済む方がいいですよね
このようなときには拡張可能なインターフェイスが役に立ちます
拡張可能なインターフェイスはマルチインターフェイスと RTTI を用いると実現できます
マルチインターフェイスは機能をそれぞれ適切な抽象基底クラスに分けることから始まります
Fig.4
#define classid_ISampleUnknown 0 #define classid_ISampleFooBar 3 #define classid_ISamplePlugin 4 #define classid_ISampleSaveLoadCallback 5 struct ISamplePlugin : public ISampleUnknown { virtual void* DynamicCast( int classid ) = 0; virtual unsigned long AddRef() = 0; virtual unsigned long Release() = 0; ... }; struct ISampleSaveLoadCallback : public ISampleUnknown { virtual void* DynamicCast( int classid ) = 0; virtual unsigned long AddRef() = 0; virtual unsigned long Release() = 0; ... virtual bool Save( ISampleSave* pSave ) = 0; virtual bool Load( ISampleLoad* pLoad ) = 0; ... }; struct ISampleFooBar : public ISampleUnknown { virtual void* DynamicCast( int classid ) = 0; virtual unsigned long AddRef() = 0; virtual unsigned long Release() = 0; ... virtual int foo() = 0; virtual int bar() = 0; ... };
こうして機能ごとに抽象基底クラスが分けてあればユーザーは自分の必要な機能の抽象基底クラスだけを実装すればよくなります
例えば Save/Load も foo/bar も必要の無いオブジェクトでは Fig.5 のように
例えば Save/Load も foo/bar も必要なオブジェクトでは Fig.6 のように
Fig.5
// Save/Load も foo/bar も必要の無いオブジェクトの実装 class CSamplePlugin : public ISamplePlugin { long m_cRef; ... public: virtual void* DynamicCast( int classid ); virtual unsigned long AddRef(); virtual unsigned long Release(); ... CSamplePlugin(); virtual ~CSamplePlugin(); }; CSamplePlugin::CSamplePlugin(): m_cRef(1) { } CSamplePlugin::~CSamplePlugin(){ } void* CSamplePlugin::DynamicCast( int classid ){ void* pvObject = NULL; if( classid == classid_ISampleUnknown ){ pvObject = ( ISampleUnknown* )this; } else if( classid == classid_ISamplePlugin ){ pvObject = ( ISamplePlugin* )this; } return pvObject; } unsigned long CSamplePlugin::AddRef(){ return ++m_cRef; } unsigned long CSamplePlugin::Release(){ if( --m_cRef == 0 ){ delete this; return 0; } return m_cRef; }
Fig.6
// Save/Load も foo/bar も必要なオブジェクトの実装 class CSamplePlugin : public ISamplePlugin, public ISampleSaveLoadCallback, public ISampleFooBar { long m_cRef; ... int m_foo; int m_bar; ... public: virtual void* DynamicCast( int classid ); virtual unsigned long AddRef(); virtual unsigned long Release(); ... virtual bool Save( ISampleSave* pSave ); virtual bool Load( ISampleLoad* pLoad ); ... virtual int foo(); virtual int bar(); ... CSamplePlugin( int foo, int bar ); virtual ~CSamplePlugin(); }; CSamplePlugin::CSamplePlugin( int foo, int bar ): m_cRef(1), m_foo(foo), m_bar(bar) { } CSamplePlugin::~CSamplePlugin(){ } void* CSamplePlugin::DynamicCast( int classid ){ void* pvObject = NULL; if( classid == classid_ISampleUnknown ){ pvObject = ( ISampleUnknown* )( ISamplePlugin* )this; } else if( classid == classid_ISamplePlugin ){ pvObject = ( ISamplePlugin* )this; } else if( classid == classid_ISampleSaveLoadCallback ){ pvObject = ( ISampleSaveLoadCallback* )this; } else if( classid == classid_ISampleFooBar ){ pvObject = ( ISampleFooBar* )this; } return pvObject; } unsigned long CSamplePlugin::AddRef(){ return ++m_cRef; } unsigned long CSamplePlugin::Release(){ if( --m_cRef == 0 ){ delete this; return 0; } return m_cRef; } bool CSamplePlugin::Save( ISampleSave* pSave ){ // 何かセーブする return true; } bool CSamplePlugin::Load( ISampleLoad* pLoad ){ // 何かロードする return true; } int CSamplePlugin::foo(){ return m_foo; } int CSamplePlugin::bar(){ return m_bar; }
そしてマルチインターフェイスのオブジェクトを扱う側のコードでは RTTI を使って適宜必要な型を得てオブジェクトにアクセスするようにするのです
Fig.7
ISamplePlugin* pPlugin = ...; ... ISampleSaveLoadCallback* pCallback = pPlugin->DynamicCast( classid_ISampleSaveLoadCallback ); if( pCallback ){ // セーブ/ロード機能を持っているから任せる bool fSuccess = pCallback->Save( pSave ); if( fSuccess ){ // ちゃんとセーブできた。 ... } else{ // セーブに失敗した。何か適切なエラー処理をする ... } } else{ // セーブ/ロード機能を持っていないからデフォルトの処理をする ... }
こうすることでユーザーは必要な機能だけを実装すればよくなり、それを使う側はユーザーに負担をかけずに機能を拡張することができるようになるのです