実装の隠ぺい
ライブラリを設計するとき、C++ では 「pImplイディオム」を使って実装の詳細を隠ぺいすることがよくある。本記事では、少し違った視点からの実装隠ぺいについて紹介していきたい。
システムと内部モジュール
ここではまず、あるサービスを提供するクラスを「System」、内部クラスの1つを「Module_A_System」と定義する。System は常に Module_A_System インスタンスを保持しているとは限らないので、メンバ変数にはそのポインタとして持たせている。そして Module_A_Systemには、具体的なサービスであるところの「Service関数」が定義されている。
class Module_A_System
{
public:
void Service()
{
printf( "%s\n", secretStr.c_str() );
}
private:
const std::string m_secretStr = "call A_System::Service\n";
};
// 前方宣言
class Module_A_System;
class System
{
private:
Module_A_System* m_pModule_A_System;
};
ここで、ユーザにこのシステムを利用してもらうために、System クラスと Module_A_System::Service 関数を公開したい。しかし、 Module_A_System クラスが private な秘密のメンバ変数 m_secretStrを持っているので、このまま公開するわけにはいかない。
Module_A_System クラスを隠ぺいしつつ、Module_A_System::Service 関数を公開する手段が必要となる。
ラッパークラス
それでは隠ぺいの手順について見ていこう。
まずは、Module_A_System クラスの代わりに、ユーザへ公開されるラッパークラスを定義する。
// 前方宣言
class Module_A_System;
class Module_A_Wrapper
{
public:
void Service() const;
public:
Module_A_Wrapper( Module_A_System* const );
private:
Module_A_System* const m_pModule_A_System;
};
#include "module_a_wrapper.h"
#include "module_a_system.h"
//-------------------------------------
void Module_A_Wrapper::Service() const
{
m_pModule_A_System->Service();
}
//-------------------------------------
Module_A_Wrapper::Module_A_Wrapper( Module_A_System* const pModule_A_System )
: m_pModule_A_System( pModule_A_System )
{
}
//-------------------------------------
コンストラクタで Module_A_System クラスのポインタをもらい、Service 関数をラップした同名の関数(同名でなくてもよい)を持たせている。
ラッパーインスタンスの生成
ラッパーインスタンスは、Systemクラスの内部で生成する。そのために System クラスを以下のように拡張する。
// 前方宣言
class Module_A_System;
class System
{
public:
void Apply( const std::function<void( const Module_A_Wrapper& )>& );
private:
Module_A_System* m_pModule_A_System;
};
//-------------------------------------
#include "system.h"
#include "module_a_wrapper.h"
//-------------------------------------
void System::Apply( const std::function<void( const Module_A_Wrapper& )>& caller )
{
if( m_pModule_A_System != nullptr ) {
const Module_A_Wrapper module_a_wrapper( m_pModule_A_System );
return caller( module_a_wrapper );
}
else {
// サービスを提供できない
}
}
//-------------------------------------
System クラスに新設された Apply 関数は、Module_A_Wrapperクラスを利用したいユーザ関数を受け入れる。Apply 関数の内部で一時的に生成されたラッパーインスタンスが、ユーザ関数の引数へ渡される。関数を抜けるとラッパーインスタンスは破棄される。ここで重要な点は m_pModule_A_System の nullptr チェックを行っているところだ。ラッパーインスタンスは、サービスを正しく提供できる場合にだけ生成される。これにより、ユーザが不適切なタイミングでサービスのアクセサにタッチしてしまうことをシステムが防いでくれている。
Usage
#include "system.h"
#include "module_a_wrapper.h"
{
System system;
const auto use_service = []( const Module_A_Wrapper& module_a ) {
module_a.Service();
};
system.Apply( use_service );
}
補足
実はもうひとつ、この手法には施しておくべきことがある。それは、ラッパークラスをコピー禁止にすることだ。ユーザ関数の引数として公開するラッパーインスタンスは先に述べたとおり、Apply 関数のリターンとともに消滅する。これをユーザ側でコピーしてしまうと以後危険な呼び出しとなってしまう。noncopyable なクラスにしておくのが親切だろう。
使いやすさとのトレードオフ
今回紹介した手法は、ユーザにとっては一見遠まわしなサービスの使い方かもしれない。もっと簡単にアクセスしたいと思うユーザの気持ちもわかる。しかし、やり方を縛るということは、不自由な反面、安全性を確保してくれているとみることもできる。ユーザに責任を負わせても小回りが利くほうがよいか、それとも制限をつけて間違いを減らすか。これから作るシステムの性質と照らしあわせて考えていきたい。