JavaではNVIを使わないの?
高木です。おはようございます。
最近、私が嫌いなAndroidアプリを作るために私が嫌いなJavaでコードを書く機会がそれなりにあります。
念のため補足しておくと、私がAndroidやJavaを嫌いなのはあくまでも感情論です。
なので、AndroidやJavaの良し悪しについて批評しているわけではありませんので、そこのところはご理解ください。
JavaでAndroidアプリも書いているのですが、C++でLinuxのプログラムの書いています。
C++はかなり慣れていますので、ある程度反射的に手が動きますが、Javaはなかなかそうはいきません。
その結果、C++では当たり前のように使っているイディオムをJavaでも使おうとしてしまうわけです。
もちろん、異なるプログラミング言語ですから、同じイディオムが通用する場合もあれば、そうでない場合もあります。
通用しないわけではないけれど、あまり適切ではないか、他の代替手段を使う方がよい場合もあることでしょう。
残念ながら、私はJavaにはそれほど詳しくないので、何が適切かはよくわかりません。
さて、今回の本題ですが、JavaではNVI(=Non-Virtual Interface)を使わないのだろうか? という件です。
C++ではNVIは普通に使うイディオムです。
簡単にいうと、仮想関数をpublicにはせず(できればprivateにして)、仮想関数を呼び出す非仮想関数をpublicにするというものです。
まずは、NVIを使うと何がうれしいのかを、C++について見ていきます。
次のような悪意のあるコードを考えてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <iostream> class base { public: virtual char const* f() const { return "base"; } }; class derived : public base { public: virtual char const* f() const { return "derived"; } }; int main() { auto p = new derived; std::cout << p->base::f() << std::endl; } |
derivedクラスはbaseクラスから派生しています。
のんきに仮想関数fをpublicにしていますね。
そうすると、main関数の中でやっているように、derivedクラスのオブジェクトを経由して外部から基底クラスであるbaseのf関数を呼び出せてしまっています。
こういうことをやると破綻するケースも少なからずあるので、こんな呼び出し方ができないように設計するのが吉というものです。
なので、次のようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 | class base { public: char const* f() const { return do_f(); } private: virtual char const* do_f() const { return "base"; } }; class derived : public base { private: virtual char const* do_f() const { return "derived"; } }; |
これで、悪意のあるクライアントコードから守ることができるようになりました。
仮想関数をprivateにしておくことで、派生クラスから悪意のある、あるいは誤った呼び出し方をすることも防いでいます。
とまあ、C++ではこんな理由もあるのですが、Javaではこんな呼び出し方はできないので問題ないようです。
それより、本当はもっと前向きな理由があります。
具体的には、仮想関数は実装する人にとって便利な仕様にして、公開関数は呼び出す人にとって便利な仕様にすることができるという利点があります。
引数のエラーチェックや排他制御などは公開関数で行ってしまうことで、仮想関数を実装するときにはそれらを省略することができます。
また、シグニチャが異なる複数の関数を多重定義する場合も、公開関数だけを多重定義すればよくなり、仮想関数はクラスにつき一つだけ実装すればよくなります。
そのようなメリットのあるNVIを、Javaで使わないのかなあ? と考えたわけです。
Googleで検索したところ、「Java NVI」とか「Java Non Virtual Interface」とかでは全然ヒットしませんでした。
「Java Non Virtual Method」にするといくつかヒットしますが、どうやら期待しているものではないようでした。
とりあえず、実際にやってみようと思い、嫌いなJavaのコードを書いてみました。
(命名がJava風ではないのは気にしないでください)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class Base { public final String f() { return do_f(); } private String do_f() { return "Base"; } } class Derived extends Base { private String do_f() { return "Derived"; } } |
このように書けば、コンパイルすることも実行することもできましたが、fメソッドを呼び出すと必ずBaseクラスのdo_fメソッドが呼び出されてしまいます。
do_fメソッドをprivateではなくprotectedにすればちゃんと動きますが、これでは脆弱性を残してしまいます。
そもそも、Javaの場合(C#なんかもそうですが)、interfaceとかNVIとは非常に相性が悪い言語仕様なので、どう考えても無理ありそうです。
どうしてもNVIをやろうとすると、Pimplイディオムと併用するとかしないと無理な気がします。
Pimplイディオムについては話が長くなるので割愛しますが、興味のある方は自分で調べてみてください。