NVI:Non virtual Interface
NVI(Non Virtual Interface)って、ご存じですか? 実装設計におけるオススメのポリシー/スタイルの一つなんですけど、そんな難しい話じゃありません。平たく言えば「パブリックメソッドをvirtualにしない」という実装上の制約です。それって何がオイシイんでしょう。
日付を扱うアプリケーションのいち部品として、こんなのを考えました:
class DateUtil { public: const char* dayOfWeek(int nday) const; /* nday で与えられた 0,1...6 に対し、それぞれ * "日曜","月曜" ... "土曜" を返す */ ... }; const char* DateUtil::dayOnWeek(int nday) const { switch ( nday ) { case 0 : return "日曜"; case 1 : return "月曜"; ... case 6 : return "土曜"; default: return nullptr; }
どこにでもありそうな、どってことのないコードです。日本語だけでなく英語にも対応しましょう。
class DateUtil_ja { public: const char* dayOfWeek(int nday) const; /* "日曜","月曜" ... "土曜" を返す */ ... }; class DateUtil_en { public: const char* dayOfWeek(int nday) const; /* "Sun","Mon" ... "Sat" を返す */ ... };
ワールドワイドな市場を狙ったアプリケーションの部品なら、これまたありがちなスペックですよね。
ユーザのお好みやlocaleの設定次第で日/英をコロコロ切り替えたいならきっと:
enum class language { ja, en }; class DateUtil { // 基底クラス public: virtual const char* dayOfWeek(int nday) const=0; static DateUtil* create(language lang); ... }; class DateUtil_ja : public DateUtil { public: virtual const char* dayOfWeek(int nday) const; /* "日曜","月曜" ... "土曜" を返す */ ... }; class DateUtil_en : public DateUtil { public: virtual const char* dayOfWeek(int nday) const; /* "Sun","Mon" ... "Sat" を返す */ ... }; DateUtil* DateUtil::create(language lang) { switch ( lang ) { case language::ja : return new Dateutil_ja(); case language::en : return new Dateutil_ja(); default: return nulllptr; }
まぁ、まずまず妥当なデザインでしょうか。
ここでNVIを適用します。NVIは「パブリックメソッドをvirtualにしない」がルールなので、dayOfWeek()をvirtualにできません。じゃぁどうするかっつーと、virtual仮想関数呼び出しのためにワンクッション置くんですわ:
class DateUtil { // 基底クラス public: const char* dayOfWeek(int nday) const { return do_dayOfWeek(nday); // dayOfWeekの"本体"を呼ぶ } static DateUtil* create(language lang); protected: virtual const char* do_dayOfWeek(int nday) const =0; ... }; class DateUtil_ja : public DateUtil { protected: virtual const char* do_dayOfWeek(int nday) const; /* "日曜","月曜" ... "土曜" を返す */ ... }; class DateUtil_en : public DateUtil { protected: virtual const char* do_dayOfWeek(int nday) const; /* "Sun","Mon" ... "Sat" を返す */ ... };
動的に差し替わるvirtual関数をprotected部に置き、publicな関数からそいつを呼び出すことで実装しています。そのままprotectedなdo_dayOfWeek()に丸投げ/横流しですから、コードを無駄にややこしくしただけに見えます。
- 「同じような処理があちこちにバラ撒かれる(Code Clone)ことを避けたい」
- 「インターフェースの変更が実装に及ぼす影響を小さく抑えたい」
を実現するスタイルの一つがNVIです。
public部にメソッドを置くということはすなわち、"使う人"にインターフェースを提供することを意味します。それと同時にそのメソッドを"作る人"に入出力の仕様を規定することになります。
この両者が強く結びついていると一方の変更が他方に影響を及ぼします。そのこと自体はvirtualでないメソッドでも同じことなのですが、virtualメソッドのインターフェースの変更はそれを再定義しているすべての箇所に累が及ぶので、影響範囲がより広くかつ重大です。
例えばこんな仕様変更:
「dayOfWeek(int nday)だけどさ、ndayが0~6以外だったらdomain_error例外を投げてくんない?」
dayOfWeek()がvirtualで、各導出クラスで再定義されていたらこの変更は導出クラスすべてにおよびます。この変更が身内の中だけならまだいいんですけど、ヨーロッパ支部のフランス/ドイツ対応チームがDateUtilからDateUtil_fr/DateUtil_deを導出してたら...
NVIならば変更箇所は基底クラスの一箇所だけ。
const char* DateUtil::dayOfWeek(int nday) const { if ( nday < 0 || nday > 6 ) { throw std::domain_error("invalid day-number"); } return do_dayOfWeek(nday); // dayOfWeekの"本体"を呼ぶ }
「ついでにndayは1~7でお願い。処理中はCriticalSectionでガードしてね。んでもって戻り値はstd::stringにしてよ」なら、
std::string DateUtil::dayOfWeek(int nday, CriticalSection* cs) const { if ( nday < 1 || nday > 7 ) { throw std::domain_error("invalid day-number"); } EntrCriticalSection(cs); std::string result = do_dayOfWeek(nday-1); // dayOfWeekの"本体"を呼ぶ EntrCriticalSection(cs); return result; }
ちょっとした工夫なんだけど、クラス・ツリーの枝葉の多い構造であったならかなりの効果が期待できますよ。