フィンローダのあっぱれご意見番 第119回「メイドとクラス」
← 前のをみる | 「フィンローダのあっぱれご意見番」一覧 | 次のをみる →
Bフレッツが開通したら、 もっと自宅のサーバの機能を増やしてみようと企んでいるのだが、 NTTがどうも乗り気でないのか、開通する気配が感じられない。 というわけで、 今のところ、 私が普段使っている情報の多くがVAIOのハードディスクに入っているというのが現状である。 それをサーバに置くことで、ある程度は他の人と共有できたら面白いのではないかと思う。 | ※ 工事の予約を入れてから実際に開通するまで結構かかったのだ。 この時、自宅近辺にはBフレッツユーザーがいなくて、 何百mか先から光ファイバーを引っ張ってくるという工事までやった。 もちろん、それで追加料金がかかる、というようなことはなかった。 | |
リソースを一箇所に集めたら管理が楽、 というのは安易ではあるが、発想自体は正しい。 それが自宅だと、外出先からアクセスできないというのが今迄の難点だったが、 ブロードバンド時代になるとその壁がなくなる。 しかし、データファイルの数が数万、数十万になってくると、 やはり管理するのが一筋縄ではいかない。 ということで、データを管理するためのソフトを作ろうと考えた。 それってDB? いや、DBをアクセスするための手伝いをしてくれるソフトである。 そういうソフトって何というのか分からないのだが、 普通は秘書ソフトとか言うのだろう。 個人的な趣味に基づいて、メイドソフトと呼んでいる。 全然深い意味はないのだが、secretary という名前よりも maid の方がタイプするのが楽だし。 | ※ もっと単純な発想でよければ、 とにかく一箇所に集めておいて、 検索エンジンを使うとか。 ちなみに蛇足しておくと、 タイトルの「クラス」は「暮らす」にかけているのである。 この頃、萌えブームだった。 | |
このメイドソフトを作るにあたって、暇だからというのではないが(暇もないし)、 徹底的にオブジェクト指向的に作ってみたらどうなるか、というのを試している。 まず、メイドのクラスを作る。スケジュールのクラスを作る。 メイドにスケジュールを渡してやると、データベースとか参照して、 その日にすべきことをメールで送ってくれる。とかいう感じのものにしようとしている。 | ||
どうせなら、後から行動パターンを追加できるようにするとか、 学習して勝手にいろいろやってくれるとか、そういうソフトにしたいのだが、 動的にクラスを追加したりする機能はちょっとC++やJavaだと厳しいような気もする。 自分自身を再コンパイルすればいいだけの話かもしれないが。 もっとも、まだ何も動いていない状況なので、単なる絵空事ということで。 | ※ もちろん Java のリフレクションとか、 Jakarta commons の DynaBean みたいな発想を使ってナニすることは不可能ではない。 ただ、ひたすら厳しい。 | |
§ 継承というのは、 オブジェクト指向で必ず出てくる考え方だが、 C++ には、 オーバーライドする関数が元の関数の型とか引数の型を正確に引き継がなければならない、 という制約を持っている。 というか、もともと継承とはそういうものなかもしれないが。 ただ、実際問題として、オーバーライドしたい関数の引数パターンが複数あったり、 戻したい結果の型が異なるということはよくあるような気がする。 List 1 で、 derived_class_long は base_class の派生クラスなのだが、 foo という関数は引数の型が違うために、 base_class の foo(int n); をオーバーライドするのではなく、 別の関数として扱われてしまう。 ---- List 1 ---- class base_class { public: virtual void foo(int n); } class derived_class_int : public base_class { public: void foo(int n); } class derived_class_long : public base_class { public: void foo(long n); } ---- List 1 end ---- 関数を仮想化するということは、 何か目的になるような操作があって、 それが個々の派生クラスによって違いまっせ、 という狙いの筈だが、 オブジェクト毎に実装したい処理の引数の型が食い違うというのは、 全くお話にならない、ということなのだろうか。 あるいは、実は現実的にはいくらでも逃げ道があるから、 言語仕様としてはこだわる意味がないのかもしれないが。 実際にそのようなケースがあるのかというと、 最初から意識して作ればないのかもしれないが、 リファクタリングしようという時には、 ここの引数が同じだったら簡単に共通化できるのに…というような状況が滅茶苦茶多々あるような気がする。 では、そういう処理が出てきたらどうすればいい? この例のように、引数の数が同じで型だけ違うのなら、 テンプレートを使う手はあるが、引数の数が違ってしまうとダメ。 そこで、もっと安易な逃げ道の一つは、ダミーの引数を使うという手である。 | ||
---- List 2 (ダミーの引数を使ってオーバーライド) ---- class base_class { public: virtual void foo(int n, long l); } class derived_class_int : public base_class { public: void foo(int n, long l); } class derived_class_long : public base_class { public: void foo(int n, long l); } ---- List 3 end ---- | ※ まさに邪道を王道で行くようなやり方だ。 | |
派生したクラスのfooを呼び出すには、foo(0, 100L) みたいな感じで、 使わない方の引数にはダミーの0という値を入れて呼ぶのだ。 実際はこの値は呼ばれた関数では使わないはずだから、 どんな値を入れてもいいのだが、 こういう場合は慣習として0か-1を入れることになっているようだ。 この手法を使えば、 派生クラスのメンバ関数はきっちり同じパターンの引数を持つことになる。 ただ、派生クラスの種類が多くなると、 ダミーの引数がどんどん増えてしまうという弱点がある。 可読性が恐ろしく低下しまくるのだ。 そろに、新たな派生クラスを追加しようとした時に、 コード全体を見直してダミーの引数を追加するような、 不毛な作業を強いられるかもしれない。 これはすごくヤだ。 § というわけで、逆から攻めるのが、引数を使わないという方法である。 C だとグローバル変数という恐怖の大王が出てきたりするのだが、 C++はCと違ってクラスを使っカプセル化することができる。 というわけで、 メンバ変数で値を渡してやればいい。 ---- List 3 ---- class base_class { public: virtual void foo(); protected: int m_i; long m_l; } class derived_class_int : public base_class { public: void foo(); // 呼び出す前に m_i をセットすること } class derived_class_long : public base_class { public: void foo(); // 呼び出す前に m_l をセットすること } ---- List 3 end ---- あらゆる引数をメンバ変数に置き換えれば、 全てのメンバ関数は引数なしになるし、戻り値もvoidだけで済む。 当然、同名の関数はオーバーライドすることになる。 | ||
しかし、これってオブジェクト指向的に正しいアプローチなのだろうか。 関数の使い方として、引数が常に無だというのは何となく不安がある。 その昔、パソコンというものを買えば電源を入れたらBASICという言語が起動した頃、 変数は全部グローバルでどこかに書いてあるのを渡したという経験が脳裏に浮かぶのかもしれない。 List 2 の場合、グローバルではなく private のメンバ変数で値を渡すのだから、 保守性はそれほど損なわれないのだが、何か体感的な抵抗があるのだ。 もう一つちょっと恐いのが、関数呼び出しとメンバ変数のセットという操作の距離である。 引数の受け渡しに比べて、明らかに弱い関係になりがちだ。 その結果、値をセットし忘れて呼び出すという事故が多発する原因になってしまう。 | ※ PC-8001 とか、そういう名前のpcがあった時代の話である。 オブジェクト指向的には、 is a の関係というのが非常に強い制約であって、 そこから外れたやり方はもちろん正しいアプローチとは言い辛い。 | |
§ そこで奥の手として出てくるのが、「なんでもクラス」を引数にするというものだ。 ---- List 4 ---- class general_class { public: int i; long l; } class base_class { public: virtual void foo(general_class); } class derived_class_int : public base_class { public: void foo(general_class); } class derived_class_long : public base_class { public: void foo(general_class); } ---- List 4 end ---- | ||
今回は遠慮して general_class に int と long の2つしかメンバ変数を持たせなかったが、 やりたければいくらでもメンバ変数を追加できる。 しかも、後でメンバ変数を追加しても、general_class の中だけの修正で済むので、 保守も楽だ。 | ※ もちろん、追加した型の変数を使う処理はどこかに追加しなければ使えない。 | |
Java だと全てのオブジェクトが Object から派生しているということで、 奥の手としてはとりあえず Object で受け渡しする、 という技があって、 受け取った側はすぐに何か別のクラスにダウンキャストしてしまう、 という感じのことをするのだが、 こういうのはちょっとオブジェクト指向から踏み外したような感じがして面白い。 実際「なんでもクラス」を使う方針の究極は、 関数の引数は全て generic_class だというプログラムである。 継承させるために関数の引数をどうするか、 ということは全く考える必要がなくなる。 しかし、本当にそれでいいのだろうか? そもそも、関数の引数というのは一体何のためにあるのか考えてみると、 何となく気になる。 パラメータという本来の姿を考えてみると、 どちからというと、引数の数も型も一致していないとoverrideできないという制限の方が、 現実世界をモデリングするという場合に限っては、厳しすぎるのかもしれない。 もうそうなるとCとかC++という枠を越えた別の世界に行ってしまいそうだ。 |
(C MAGAZINE 2002年5月号掲載)
内容は雑誌に掲載されたものと異なることがあります。
修正情報:
2006-03-03 裏ページに転載。
(C) Phinloda 2002-2006, All rights reserved.