継承とは?
copyright 1996
Kubo
C++等の継承の罠。設計・実装・能力・開発時間等のバランスを考えよ。
何が言いたいのかはページ末。
NetNews発言を再編集しました。
fj NetNews--> fj.comp.lang.c , fj.comp.lang.c++
文章の繋がりが変かもしれませんが想像力でお読み下さい。
問題提起
- 三角形(図形)から描画は派生するのか?
いいえ、図形と描画は異なった継承図になります。だから、三角形自身が描画する構造は設計上おかしいです。描画クラスに三角形を送ると三角形を描画するのです。
色々な人のクラス設計を見ると、継承を勘違いしています。何でもかんでも継承してみたり、「どうしたら継承ですっきり行くか」とか、オブジェクト指向と継承を混同している部分が有ると思います。
まず、設計と言うからには、全体の把握が必要です。
何をしたいのか?
どの程度、耐久力のあるクラスを想定しているのか?で設計も変わりますが、三角形に特化しているなら、
三角形クラス(自身のこと、自身と自身の演算までにととめる)
三角形(図形)描画(三角形クラスを与えると描画)
図形関係解析クラス(三角形クラスを使って関係情報を管理)
程度のことだと思います。
継承には2つのパターンがあります。
その1、継承によって少しずつモデルが複雑になるパターン。
データの加算によるモデルを確定的に使用した方が全体にとって良い場合。
その2、モデルの性質を基底に置く、インターフェイス・パターン。
モデルを抽象的に捉えた方が全体にとって良い場合。
どうも多くの人は、パターン1が解りやすいせいか、なんでも派生させて機能を増大させていきます。
実際には、パターン2を念頭において上手にパターン1を組み合わせると良い設計(わかりやすい)になるのですが…。
パターン1を多用すると、継承途中を間違えた場合に詰まります。
だって特殊なことを無理矢理追加して多くの固体を作るから。で、その固体同士を何かしようとすると矛盾が…。)しかも、持つのか、継承か、も、いい加減だし。
私だったら、図形の抽象クラスから三角形を派生。
描画も「図形の描画」と言う抽象クラスがある。
「図形の描画」に「図形」を渡すと図形に応じて描画。
図形の関係を扱う場合。
自身と自身の演算までを1つのクラスで出来る範囲とする。
自身と複数の自身との関係、自身と他の図形との関係は、
既に特別なことになるので、新しいクラスが必要。
自身と複数の自身との関係。(三角形なら)
三角形の隣関係を表したクラスを新規に作成。
そのデータに三角形クラスを入れる。(三角形と隣の構造の完成)
隣を使用する演算は、専用の演算クラスが新規にできる。
あるいは、「三角形と何々の演算を行うクラス」があって、それから派生。
もちえろん詳細が分からないので、この限りではないですけど、ポイントは「この派生は別だ」と思えるか?です。
三角形の拡張を、
描画もできる。
隣もわかる。
何々も…。
と考慮すれば際限が無いのです。
「三角形は三角形でしょ」おしまいですよ。こう割り切れない原因は、パターン1の継承が強いからです。
パターン2だと、「図形でしょ、図形に出来ることは…」と考え、継承の行きつく先は何らかの図形で止まるはずです。
ただし、図形のメンバに描画は入りませんよ。
では、複数の性質を継承して多機能なクラスを作成する。
言い換えると「何々は、何でもあり、何でもある」と考えてください。
#「何と何の性質を併せ持つ」じゃないですよ。これならクラス内に持たせれば良い。
ラジオ・クラスとテープレコーダ・クラスの両方を継承させ、ラジカセ・クラスを作成する例が似た例ですが、これは2つのクラスを持たせるべきです。
つまり、世の中に2つの性質を踏襲して新しい性質になる例はありません。SFならあるかもしれない。
細胞の共生を例に出す人もいますが違うでしょ。と言うことで、私は複数のクラスから1つのクラスが継承されることを推奨できません。
ただし、現場の設計レベルでは混乱の元になるので禁止です。
それに派生で詰まってしまうケースがほとんどです。
両方の性質に揺れ動くクラスは将来性に不安があるのです。
更に問題なのが、(こちらの方が重要)
・三角形クラスがあります。
Q.「三角形クラスから描画を派生させる」のと「三角形クラスから三角形の付加情報を持ったクラスを派生させる」のでは、どちらが自然ですか?
正解は「三角形の付加情報を持ったクラスを派生させる」です。
ですから、三角形から三角形の隣人情報をもったクラスになるのは問題がありません。
では、三角形の描画クラスに派生していて、更に三角形の付加情報(最初の質問では隣接情報)による描画がしたい場合。
この場合、クラス継承に詰まってしまいます。これは、誤りであることが分かります。
#継承の遣り方を変えてありますが、結果は同じになっちゃう。
#仮想関数にしても無駄なコードを書かないと実現できません。
#しかも、どの時点で描画クラスにするの?
#例としては良くないですが、三角形の継承で三角柱を想定。
#この場合、三角形の描画から三角柱が継承される?(->変ですね)
#三角形から三角柱を派生、三角柱と描画から三角柱の描画が派生?
#(->三角形の描画のコードは無駄になる。できてもそのクラスを使う気に…)
#それとも、上の継承にもう一つ三角形の描画からも派生させる?
#(->もはや設計とは言えないですね。つじつま合わせ。)
私は、「三角形を図形として扱うなら」と言いました。
これが三角形のクラス継承の前提条件となります。
では描画は違うのか?--->違います。
三角形は三角形(付加情報も)の知りうる情報を返せば良いのです。
自身の演算に拡張して考えても、面積は三角形の情報です。回転も新たな三角形の情報です。
ところが、描画は三角形の視覚化です。
ここ重要です。良く考えましょう。
「描画」とはなんでしょう?既にある何らかのデータを見せるものです。
何故、三角形と言う観念が頭の中でできているのに、わざわざ描画させるか?
#頭の中に三角形を書いているでしょ。
#だから視覚化する必要の無い演算等の継承経路が無理なくできる。
つまり、何かの媒体(頭の中以外)に具体的に表現するのが描画です。だから、三角形は自分を表現するもの(クラス)に飛び込むのが普通です。
設計を重視するなら(と言うか重要です)、全体(将来)において矛盾があってはいけません。それは難しいことでなく、クラス(言葉)を自然に組み合わせて全体が語れれば良いのです。
つまり、誰にもわかるように日本語でシステムが話せればOKです。
それは大抵、正解です。
#案に描画とかを三角形に連動させて、その三角形を将来何らかの
#システムに組み入れて使用する場合矛盾が生じることを事前に察知しないと…。
描画継承をもう少し
- 継承万能の思い込み。
・昔は図形に描画機能を持たせていました。(事実)
・しかし、図形群コレクションを想定したシステム(CAD)の設計時に設計の見直しをした。
・その時、多重継承とか色々試した。(悲しい技術者の性)
・結果、図形は図形の基礎データだけである方がすっきりしている。
・理由を考えた。(図形に関するシステムに耐えられるクラス群だった)
重要なのは、描画を止めた理由がうまく伝えられるかですね。
「三角形以外に色々な図形が、図形抽象クラスから派生している。」
上記を全体を通してこれを前提に記述します(重要なファクターでないですけど)。
ちなみに、局所(簡単なシステム)だけに目が行くと全てが正解と思えちゃいます。
と、前置きはこれくらいで…。
----まず、派生は抜きにして、機能を持たせるべきか? から…。三角形クラスを使用して大きな設計をすると思ってください。
1.三角形クラスはデータのみの扱いが出来るクラス->と言う範囲。
2.三角形は描画までもできる。
明らかに「2」の場合は、三角形の範囲に広がり(可能性)を想像させ、クラス仕様書に目を通したくなります。(他に何があるんだろう?って)
すると、システムを日本語で簡単に想像できなくなります。「三角形が」と「三角形で出来ることは」ではちょっと違ってきます。
つまり、図形、座標、何々…と言う物体の情報に限定させる…。こすることにより、システム全体を読みやすくなります。
じゃぁ、例えば車クラスを作成した場合。
「”走る”とか機能が付くじゃない?」と言われそうです。
そうです、車とかちょっと大き目のクラスには動作が付いても良いと思います。
なぜなら、システムを通して特別(と言うかなんて言うか)だと割り切ります。
#今回の三角形の関係の派生は「私としては賛成できない」
#ただし、質問者が「特別」と言いきるなら、それも厳密には間違いではない。
#そのかわり、将来の保証はしませんよ。「美しくない」と言うレベルかも。
#「持たせるか、派生か」の議論の先を話しています。
私は図形クラスを他のクラスを作る際の基本クラスと言う位置づけにしたい。
図形クラスは、文字列クラス・配列クラスと同レベルのコレクションである。
この時に、「私は図形をそんな(コレクション)ように扱いたくない」と言われれば
ここから、議論がかみ合いません。これに同意が無いと次の議論(継承)に移れない。
ただ経験から、図形程度なら自身のコレクションに加えるのがよろしいかと…。
「描画可能な文字列は文字列から派生している。」
さて、文字列から文字列描画クラスを派生させますか?
リスト構造クラス->図形の扱えるリスト構造クラス
さて、図形群(リスト)を描画できるクラスを派生させますか?
また、三角形に限定して議論していますが、(その前に描画の目的だけで派生させるのも…と思える)
円から円弧(始終点が追加データ)を派生させましょう。
描画の派生があるならこれはもっと妥当。
円から描画データを派生させると、円弧の描画は?
と言う、三角形の議論と同一になります。
車クラスの場合、「走る」は特別だからと言いました。厳密には矛盾しています。
しかし、全てのクラスに描画の派生の可能性を残すのと、描画は別にあって、何らかのクラスを描画に渡すと描画する。
どちらが全体にとって理解しやすいですか?
更に言うと、図形クラスがある。
図形クラスを描画するのがある。
これらの基礎情報が他のクラスでも共通してある方が全体を見やすい。
・三角形(これに順するような)はデータのみ。
・クラスは描画に渡すと描画する。
これらの暗黙条件がシステム全体を話すときに、余計な作業(一々仕様書を見るとか)を減らす(暗黙だから)。
システムを語る場合には、細部にこだわりません。
ですから、細部のことは連想(こうなっているのだろう)させて話します。
結果的にそれが設計に反映されるのです。ですからプログラムも違和感がありません。
ただし、連想の範囲が人それぞれ微妙に違うのも事実です。
その違いが少ないケースを模索するのがクラス設計の妙です。
#当然、基本的なクラス設計が出来ることを前提としますが。
どちらかと言うと、継承でクラスが存在するのと、部品みたいなクラスが単独で存在するのとでは、後者の方が自由度が大きいです。
#良い設計かどうかは別として。
ですから、クラス設計に慣れていないで、C++のプログラムを作る場合、「継承に頼りすぎるな」と助言したいです。
クラス設計を行う場合、最初はプログラムの分野(経験)から設計を行うと思います。(ボトムアップ)
しかし、そのうちに全体を簡単な日本語で自然に分解できれば、それをクラス設計に反映させるのが必然的になります。(トップダウン)
#トップダウンをしているよ。と言う人もいますが…さて?。
そして、ボトムアップとトップダウンを時計の振り子のように振幅させ繰り返す…。
振り子は次第に振れが小さくなる、また問題が…。
振幅をちょっと大きくしてみる。
繰り返して…。
振幅が停止したときがクラス設計の終了です。
各種参考書では、物体のクラスに描画があります。
見た目にもその方がカッコイイです。
#参考書は実践向きでもありませんね。
#クラスを説明するにも簡単だものね。読者はその気になれるし。
特別な場合(ゲームのキャラとか)は有効かもしれない。
ただし、図形は違う。->演算されるのが通常の使い方だから。
- さて、どう考えれば良いのか?こんな解釈はいかがでしょう?
三角形の物体が暗闇に存在している。(クラスの概念がある)その三角形は、暗くても色々出来る。(もちろん派生も)
ただし、描画と言う行為は外部からの要因である。つまり、光が当たらなければ存在が確認できない。
そこで、その光が赤だったり、青だったりすると見え方が変わる。
もちろん三角形そのものが、光の色に対応したインターフェイスを完備していると描画の対応ができる。
しかし、光の多様さは色々ある。広がりもあるし、直進もある。
三角形は、光が当たって見えるのであって、自身がその見え方をコントロールする事はおかしい。(発光していれば別?)
三角形は、多様な見える環境に置かれることで見える。鏡のように対象に見える世界でも良い。
データがきちんとしていれば見え方はその世界観に反映される。三角形をOHPの裏から投影して見るかもしれない。
以上が、三角形(図形)は、概念であって描画は派生する必要の無い理由になると思う。
更に言うと、三角形のどこにも描画は存在してはいけない。
描画の抽象クラスを取り込んだ多重継承であっても結局は、上記のジレンマが付きまとう。(状況把握の問題)
ある種決められた状況下での描画はこの限りではないが、システム全体、将来性等の大局から見ると「おかしい」と思える。
こうなると、参考書的に Is-A、Has-A とクラスを設計する以外に直感的な洞察(経験)が必要かもしれない気がしてくる。
もう一つ付け加えると、システムを実現させるのはコンピュータ上と言うことである。この前提も忘れがちである。
上記の描画は、実現先をコンピュータ上としても変ではないが、楕円から円が派生するのは変だと思える。
概念的には正しいのだろうが、コンピュータの論理を加味すると妥当と思えない。
つまりは、以上2つのバランスでクラスを見る(振り子の振動)設計が重要ではないのだろうか?
以上は人間の観念による継承の考え方である。つまり、設計論の考えから継承を作る。
実装とインターフェイス
- 例えば、今回の三角形。
将来、図形として拡張していくクラスにするのか? 今回はここまでなのか?
なんて議論があって、じゃ、描画はどうするの?って話が続く。
すると、今回は図形として扱っていくクラスをきちんと設計しようとなる。
#こうなると話しは大きくなる。だって「将来起こり得る事象に対応できる強度のあるクラスをつくろう」ってことだから。
#大部分を本来の目的でないことに時間を割くかもしれない。
で、紆余曲折があって「描画は派生じゃないな」と見えてくる。
ここに至る道のりが実は大変で、きちんと言葉で最終目的の意味が理解できていないと方針を誤ってしまう。
ここまでが私の言っている設計。(基本設計)
以降の作業は、プログラム設計で、上記の大前提があって生成されるクラスになる。(詳細設計)
だから、今回の目的が独自で、例えば描画を継承した方が良いならそれでも良い。(描画が派生しても、基本設計に従えば間違いではない。)
しかし、「図形・文字列」なんかは描画を派生しない方が結果的に正解であり、システムを記述しやすい。
#でないと基本設計ミスと言われかねない。
こうなると、感性の問題ではないかと思えるときもある。
初期段階で「図形から描画は派生しない方が良い」と言うと瞬間的にシステム全体(将来)をシュミレーションして「ああ、そうだね」と理解できる人もいる。(この作業が見えない人もいるのです)
または、自然に自身で「なんか変」と気が付く。頭の中でしっくり来ないのです。
システムを2つ例に挙げましょう。
1.インベーダーゲームのような平面ゲームの制作。
・キャラクタがあって、それから描画が派生する。(よさそうです)
・描画の抽象クラスがあって、キャラクタが派生する。(これも面白そうです)
この例では、各クラスが描画を受け持つとシステムがすっきりしそうです。
平面的なゲームに関して将来性もありそうだし…。
しかし、平面的なゲームの領域を出られないかもしれない。
2.ポリゴンゲームの制作。
・各部分(腕とか足)がそれぞれ描画を受け持ったらどうでしょう?
ちょっと専門になりますが、Zバッファによる陰線処理の時。
視点の後方から各部分を描画していけば(面倒だが)、できないこともない。
凹図形が入った複雑な形状がある場合。(おそらくお手上げ)
「1」のように限られた範囲なら物に描画があるのは有効です。
この範囲なら間違いではない。
しかし、前にも言ったように、「クラスと言う概念があり、それは目に見えないけど頭の中に存在している(物理的にはデータがある)。」
ここまでで十分なのです。
「描画=目に見える」ことは概念の物質化なのです。この動作を基本となるであろうクラス(図形とか文字列、腕とか足)が受け持つ必要(関数・継承)はないのです。
そこで「2」のケース。
腕とか足の陰線処理はその位置関係が必要です。
位置関係クラス(人物クラスかもしれない)が腕とかのデータを受け取り、その中で描画クラス(腕そのものを書くとは限らない)を使用して、ポリゴンを描画する。
三角形の関係にも似ています。
反論で「2のケースはそもそも最初から各部に描画なんて考えるのが変じゃない?」と思いました? 例として適切じゃないとか。
そう思うのでしたら、「図形も描画を考えるのが変」と言う言い分も分かると思います。
少なくとも私には「変」なのです。プログラム云々でなくて、感性が「変」と言っているのです。
つまり、システムを設計する上で変だと感じているのです。
図形・文字列・腕は観念で良い(そういうクラスが良い)。
描画(物質化)は、別のクラスの担当(位置とか連携とかあるので。良い例がウインドウ)。
従って、「1」のケースも
平面のクラス(フィールド)
キャラクタクラス。
キャラクタ描画クラス。
ゲームの制御クラス。
があって、
#あそうか、「Windowsなら、CWnd、CDC、CRectがあって、その関係と同じです。」
#で話しは通じるか…。
キャラクタ自身が描画する必要が無いかもしれません。
- 実装を考えてみる。
通常、図形データのみで描画、あるいは何かに使うと言うことはありません。
図形クラスは何かを実行するために実態となって何かのクラスに使用されます。
例えばそれは、
図形クラス(抽象)---三角形クラス(派生) があって。
で、システムで使用するクラスは、
図形データ{
図形クラス* Figure;(ある図形クラス)
図形クラス* Range;(レンジも図形から派生)
属性クラス* Attribute;(線種・色・属性)
};
ちょっと大袈裟ですが、こんな感じ。
これが、システムの図形データなのです。
で、この図形データは、自分が何の図形を持っているか知っています。
そこで、無理に描画を入れるとしたら、「描画(抽象)---三角形描画」があって、描画(抽象)クラス*
Draw;(何を描画するか知っている)
みたいな、メンバを入れれば回避されます。
三角形をデータまでに留める決断が上記のような柔軟さを持ちます。
描画はどこの、どのシステム(OS)に行われるか、全体を設計しても将来わからないからです。
結局、絶対大丈夫なデータ群-->ライブラリー化。
不安な描画--->各(例えば)OS用のライブラリー化(修正もじさない)。
が一番、賢い選択なのです。
もしかして、X-Windowのプログラムが評判良く、Windowsに移植する。
さて、どのような構造にしておくのが良いのでしょうか?
#この例では、「抽象化をきっちりすれば問題ない」と言われそうですが、本当に修正なしで他のプラットフォームに乗せられるか不安です。
#しかもDOSに移植ってなったら、自分でGUIを作る所まで行っちゃう?ひえーーっ。
#それより、全体の構造が変わらない(自信のある)設計の方が良い。
論理を壊すと、自身が描画するのも、継承して描画するも間違いではありません。
ただ、クラスを資産として再利用する場合、その構造で本当に最後まで(色々なケースで)大丈夫か?
と言う疑問があり、私としては、「そのような設計にならない」と感じたのです。
基本設計が決まればクラス構造も自然に決まり、自分の中にも「ああ、この場合は派生だ」とか、「ここは持たせよう」なんかがポリシーとして決定できます。
#いわゆる一般的な、isとかhasの話ではありません。
#感性で意見が分かれるような場面で有効です。
最初、私がオブジェクト指向に触れとときは、抽象化によるクラス継承が楽しすぎて色々無茶をしました。(単発のテストプログラムですが)
基本クラスからいくつものクラスを派生させて、またそのクラスが継承でまとまったり、しかもそのクラスが別の継承クラスと多重継承したり。
「もしかすると継承のみでプログラムが組めるのでは?」とも思いました。
しかし、少しずつ規模の大きなシステムを作り始めると、行き詰まるのです。
きれいな継承だと思っているのにどうしても糸が解れない。
何回もソースを組み替えてみてもうまく行かない。
使いたい処理に到達できず、同じコードを書くケースになる。
「こりゃ正しい継承じゃないぞ、どこかおかしい」なんてね。
つまり、現在のプログラム体系の中で「本当の人間の思考をプログラム化できない」ことを認識するべき。
図形構造を例に「実装優先」で設計してみる。
- 何せ、部分だけ記述しても意味の無いことだから。システムの限定範囲を、まず決めましょう。
2次元のワイヤーによる簡易CADシステム、あるいは準ずるシステム。
もちろん、本格的なCADにも移行できる。
その中の図形構造の実装次第。
(図形構造はデータの追加方向を継承とする)として、一気に書き上げますか。
#仕様説明でなくプログラムで書いてみます。
注意:テンプレートの使用はしません。
//////座標クラス群(3つのタイプが考察できる)
class PCoord{
virtual int GetX();
virtual int GetY();
};
class MCoord{
virtual double GetX();
virtual double GetY();
};
class DCoord{
virtual long GetX();
virtual long GetY();
};
class P2Coord : PCoord{ //CRT,Printer,Plotter
int x, int y;
};
class M2Coord : MCoord{ //Math
double x, double y;
};
class D2Coord : DCoord{ //Memory, File
long x, long y;
};
PCoordは、GUIのデータを担当。(画面・紙等のデバイス)
#縮尺を考慮する。
MCoordは、演算を担当。(単位は不定。メニューの単位表示に従う)
#ベクトル、マトリクスに使用
DCoordは、誤差を考慮したUNITと単位を担当。(無くても良い。)
#long 10000->UNIT=1000,単位mm->10.00mm
////////////////////////
//図形ベース(将来サーフェイスモデルに対応するために)
class FigBase{ //必ずしも必要ではない。
virtual WORD GetTypr();
………
};
class PFigure : FigBase{ //ディバイス用図形の抽象クラス
virtual WORD GetType();
virtual PCoord& GetSP();
virtual PCoord& GetEP();
……
virtual PCCoord GetTP( index ); //多角形で使用(ベジェ、スプライン)
……
virtual PFigure* CreateCopyObject(); //自身の生成
virtual MFigure* CreateConvertObject( 縮尺 ); //演算用に変換
};
class P2Point : PFigure{
P2Coord sp; //座標を持つ構造。この方が良い。
P2Coord ep;
virtual MFigure* CreateConvertObject( 縮尺 ); //<-重要
};
class M2Point : MFigure{
M2Coord sp;
M2Coord ep;
virtual PFigure* CreateConvertObject( 縮尺 ); //<-重要
virtual DFigure* CreateConvertObject( ユニット ); //<-重要
…演算とかいっぱいある。
};
重要:ディバイスP <---縮尺---> 演算M <---UNIT---> データベースD
こんな感じで各種図形を派生させる。(データの追加が派生の方向)
楕円は、円->楕円で良いと思う。(おそらく矛盾しない。変だがソース効率が良い。)
考慮する図形は、えーーと。
点・マーク・線分・レンジ(矩形)・円・円弧・楕円・楕円弧・開いた折れ線・閉じた折れ線・曲線(カーブ・スプライン・ベジェ・Bスプライン・バットランド)
文字列・(1行だけの文字列があっても良いかも?)-->const定義で十分です。星型---->部品構造です。
そうすると、
M****構造を例に取ると
McFigure:図形全般を扱う派生用基本クラス
|
+--M2Point:2次元の点クラス
| |
| +--M2Mark:2次元のマーク・クラス
| |
| +--M2Circle:2次元の円クラス
| |
| +--M2Arc:2次元の円弧クラス
| |
| +--M2Ellipse:2次元の楕円クラス
| |
|
+--M2EllipseArc:2次元の楕円弧クラス
|
+--M2Line:2次元の線分クラス
| |
| +--M2Range:2次元のレンジ・クラス
| |
| +--M2Strings:2次元の文字列・クラス(領域内に編集文字)
|
+--M2PolyLine:2次元の開折れ線クラス
|
+--M2Polygon:2次元の閉折れ線クラス
描画に付いて、抽象構造は意味が無いので不採用。
以下の構造はコードの労力から言えば、派生とたいして変わらない。
ただし、保守のためソースの書き換えあり。
その代償として、プラットフォームの変化に強い。
PFigureWin{ //C言語のようにコーディングすれば十分
Draw( P2Point& );
Draw( P2Line& );
Draw( P2Arc& );
図形の増加は関数の増加。(約束)
Draw( PFigure& fig ){
SetPen( 描画属性 ); //こんなの必要だね。
switch( fig.GetType() ){
case FIG_POINT:Draw( (P2Point&)fig ); break;
case FIG_LINE:Draw( (P2Line&)fig ); break;
case FIG_ARC:Draw( (P2Arc&)fig ); break;
}//図形の増加はcaseの増加(約束)
};
以上のクラスが、不測の図形増加時に変更されるソースである。
重要なことは、システム制限事項があるので、全体でできる
システムの基本設計は終了している事。(読み切れた)
以下、サービス機能として、
MFigureWin : PFigureWin{ //演算図形も描画できる
Set( 縮尺 );
Draw( MFigure& mFig, 縮尺=1.0 ){
PFigure* fig = mFig.CreateConvertObject( 縮尺 );
PFigure::Draw( *fig );
delete fig;
}
};
こんな感じで、使うときは、
M2Coord sp( 10.0, 20.0 ); //座標定義(ベクトルとかに渡せると便利)
M2Coord ep( 40.0, 70.0 ); //座標定義
M2Line line( sp, ep ); //線分定義
MFigureWin figWin( 縮尺=10.0 );
figWin.Draw( line );
M2Coord cp( 0.0, 0.0 );
line.Rotation( cp, 40 ); // cp中心で40度の回転(こんな事も可能)
figWin.Draw( line );
ここまでが、基本部分。
システム使用図形データ群。(線形リスト構造で管理)
DBFigure* が、リスト構造に結合される場合
class DBFigure{
DFigure* fig;
DFigure* range; //あると便利。検索時間の短縮に使える?
ペン属性
};
これが例えば、DBListで使用される。(描画も組み込まれる)
class DBList : List{
Draw(); //図形群全ての描画
DBFigure* GetDBFigure( index );
Add( DBFigure* );
};
−−−−−−−以上がシステムにおける図形構造
さて、ここで描画が図形から派生している場合の不具合は?(1例を・・)
例えば、どこかの処理で描画をするが演算もするので、Pointから派生したDrawPointを使用したコーディングをする。
DrawPoint dPoint;
dPoint.演算;
dPoint.描画;
・この描画が何かに特化していたら。
・このソースを違うプラットフォームに移したい。
描画を抽象化で作成していない限り、別ルートで違う描画クラスを
Pointから派生しなくては行けない。
#PrinterDraw::Point
そうすると先のソースは、
DrawPrinter dPoint;
dPoint.演算;
dPoint.描画;
の書き換えが必要になる。
お分かりのように、システム全体では、このような展開のソース部分が非常に多いです。そのソースの管理は大変です。
では、PrinterDraw::CrtDraw::Pointとしますか?
おそらく、しないでしょう。
描画と言う観念は、概念の物質化です。どこに実態を結ぶのか将来の予測がつきません。だからクラス継承にすることは危険性を含んでいます。
以上を回避するために、描画を抽象化させる。
・別ルートに描画系統のクラスを継承させる。(私が示した例)
システムのデータを扱う場面では回避できる。
#「システムになった場合」と言うのが重要です。
#この記事で言えば、DBFigureを作成するときに何を描画するのか知っているので、DBFigureに描画抽象を追加できる。
#しかし、プログラム的に可能なだけであって、設計上わかりづらいです。
・多重継承の助けを借りて描画を継承させる。
PrinterDraw::DrawObj
CrtDraw::DrawObj
で、
DrawPoint:: DrawObj, Point
この場合も、DrawObjを使用する際、どこかでDrawObjに対して「どこに描画するのか」を教える必要がある。
例えば、DrawPointを使用するクラスが、DBDrawだとすると、どこかで一度DBDraw::SetDevice(
PRINTER );なんてなるのかもしれない。こういう場面は相当数になるかもしれない。
では、うまく描画のみを扱うように、ソース上でシステムの描画関係を集中させたら…、
おそらくその構造は、「描画を使う」に近いイメージになる。
じゃぁ、もっときっちりクラスを制作するとして、システムの図形のデータ群を扱うクラスも描画をうまく継承させる?
もはや、もつれた糸をほぐすだけで先に進まなくなります。(いつかはできるでしょうけど)
問題の個所は、点から円が継承されたりする部分です。オブジェクト思考を敬愛する人から見れば妙な継承に感じると思います。
思考的な設計が必要と私も言っておきながら、変な継承をする。これはいったい?
- どうも皆さん教科書的にするのが好きなようなので…。しかも、概念の解釈に振り回されているし。
基本的な考えは、*これ以上図形は派生*派生はしない。
概念で設計(皆さんの言う)->データ効率を考慮->実装時の矛盾を排除。
おそらく、実装しても何ら問題は出ないでしょう。(自身もあります)
データの追加が派生の方向。図形はコレクションとして使用する。
#実装で矛盾しないで、ソース(メモリ)効率を追求すると上の継承図になるかと。
#まぁ、やりすぎていることは認めます。特にLine->Rangeなんて傑作ですね。:-)
#ソースになったときのことを考えて「面倒さ」を排除しすぎたことも認めます。
#でも、「図形=部品(コレクション)として実装」を想像すると矛盾しません。
#なぜなら、使用者は派生構造を知らなくても実装できるから。(そういう前提)
#だから、上の継承は図形を完成させるときのソース向けの継承と言えます。
#図形は、図形。これ以上どうしようと言うのでしょうか?が基本事項と定義してあります。
示したいのは、部分的な正当性ばかり議論してもしょうがないと言うこと。
言語学者ではないし、実現先はコンピュータだと言うこと。
#理想を追い求めるなら、それでも良いが。
事象の派生を、is-a とか has-aだけで議論するのも違うと思う。
厳密性を求めると、コンピュータでは立ち行かないときがある。
くどいようですが、実装は現在のコンピュータ上で行います。
#もちろん、「is-a とか has-aその他の基本」を押さえることが前提です。
自然数と整数の関係もそう。
例えば、楕円->円を肯定すると、自然数->整数になる。
しかし、コンピュータは有限値である。(既に概念と矛盾)
だから、継承の基には、誤差を認めたクラスが来る。
もしくは、対処できる設計にする。
#P、M、Dと分けた意味を考えてみてください。
教科書的に答えるのは簡単です。
そのような答えを投稿する気にもなりません。
#「参考書等を移して自分の考えを追加してみる」(まぁ、一般論でしょうが)
例えば、画面。演算では、0.99999と1.00001となる。(本当は1.0)
この場合、表現(画面)では1dotずれます。
もちろん解りやすい部分で説明していますが、実際のシステムや解析等に添加する場合、クラス自身(あるいはコレクション)が吸収していることが重要です。
#誤差(デバッグ)を考慮しないシステムが多いのが事実です。
#そして、何故かプログラムの途中で「突然」考慮する。
#(おかしいと思ったからでしょう。つまり設計ミス。)
例えば、数万の図形データを扱う場合。
楕円->円なら?、実際、楕円・円のデータは全体の何%を占めると思いますか?
その時のメモリの無駄は?(これらも設計の要素)
#しかも、データを伴わない派生も気味が悪い。(感想)
例を挙げてもきりがないですが、残念ながら現在のコンピュータでは制限事項が多いと言うことを認めないと。
設計とは、時に矛盾もあるのです。
しかし、その過程で色々試行錯誤をしている事実は理解してもらわないと…。
- ・円は点ですか?
厳密には違うでしょう。
・「円は点である」と「円は点(中心)を持つ」との違いは?
上は両方違います。円は、中心の座標を持つのです。
#点を持つ(である)のではありません。点!=座標。
#どこかに書いたと思うが継承とオブジェクト指向を混同してはいけません。
#図形から描画が派生したり、楕円から円が派生したりする考えのどこが問題点と私が考えているか良い例だと思うのですが…。
図形に関しては、難しく考えても(と言うより律義?)仕方がありません。
あれはあくまでソースを作成する際の継承図です。
仕様書には、
McFigure:図形全般を扱う派生用基本クラス
|
+--M2Point:2次元の点クラス
+--M2Mark:2次元のマーク・クラス
+--M2Circle:2次元の円クラス
+--M2Arc:2次元の円弧クラス
+--M2Ellipse:2次元の楕円クラス
+--M2EllipseArc:2次元の楕円弧クラス
+--M2Line:2次元の線分クラス
+--M2Range:2次元のレンジ・クラス
+--M2Strings:2次元の文字列・クラス(領域内に編集文字)
+--M2PolyLine:2次元の開折れ線クラス
+--M2Polygon:2次元の閉折れ線クラス
…
とでも書いておいても良いし…。ある程度概念でも良いし…。
図形の最終形で何が出来る(インターフェイス)かが重要です。
#当然、各図形からクラスを派生させても問題は起きません。
#もちろん、プログラマが本クラス群を無茶に使用しても問題ないです。
そこで、この例のMcFigureで何が出来るか?ですね。
抽象クラスとして機能します。面倒なので座標に限定して記述。
virtual MCoord& GetCP(); //中心座標の取得
virtual MCoord& GetSP(); //始点座標の取得
virtual MCoord& GetEP(); //終点座標の取得
その他は、自身の情報(面積・長さ)、自身の変換(移動・回転・誤差変換)
自身と自身に関わること(Common・Fusion)に限定。
さて、ここで図形のルールを決めてやります。
中心の座標を得られる図形-->点(決め事です)、円、円弧、楕円…。
始終点の座標を得られる図形-->線分、円弧、楕円弧、矩形(決め事です)・・。
#「点の座標を得るのに中心を要求するの?」と言う議論は無し。
以上を考慮して仕様書として出す継承図は?
実際にプログラムするのに適した継承図は?
#もちろん概念の継承図もありますよ。
と言う考慮があって…(ああ、めんどくさい)…です。
上記の考えでは半径等に矛盾が無くシステムが構築できることがわかるでしょう。
#解らなければ、私の全発言を読み返してください。
#それで解らなければ私に文才がないのでしょう。
C++をOOPと思っても仕方がありません。
C++でクラスを作成する以上、OOPも解っている、もちろんC言語も。
そして、「じゃぁ、C++でどのようにクラスを作成するのがいいの?」
とか、「ここは本当は継承だけど、持つ方がしっくり来る」(あるのかな?)
とか、「概念ではこうだが、クラスではこうだね」と言うようなノウハウが大事なのです。
C++でシステムを構築するのに、できもしない概念でシステムを構築しても、実際の作業では意味が無いと言うことです。
#できもしない概念と言いきる見識も必要ですが。
結局何が言いたいのか?
- NetNewsの発言を再編纂したので、意味がわかりにくいかもしれませんでした。
1.継承図は頭脳をフル回転してきちんとした理屈をつけるべき。
図形は描画を担当するのか?-->するわけありません。発光した図形ならあるかもしれない。
参考書等には「自身の描画」と言う、メソド・継承があるが、現実から離れている。つまり正しくない。
2.継承を使えば言いというものではない。
継承は適度なバランスで切り上げるべきである。また、多くのクラスは、「データコレクション」的な使用法をするべき。
「つまり、〜するもの」をクラスとして増やしていけばプログラム資産が増える。
3.継承には、多様化方向とインターフェイス方向の2パターンある。
特にインターフェイス方向には注意するべきである。
なぜなら、データの追加である多様化は問題が少ないが、インターフェイスには以下の問題が付きまとう。
4.実装を優先するのか、観念を優先するのか?
派生の方向に円と楕円の関係があります。
観念ならば、楕円の特殊形が円だから、楕円->円の派生。
しかし、円の構造には楕円に必要のないデータを持つ必要がないと思われます。
つまり、インターフェイス観念でも、データ追加を重視した、円->楕円の派生のほうが良いという意見。
これは最終的に、使用されるシステム等の問題になりますが、現在のコンピュータを使用する限り実装優先が良いと思います。
5.保守管理。
継承を多用したり、継承に酔っている人のクラスは使い物になりません。
特殊なことをしようとしたり、継承をどうにかしようとして結局糸がもつれます。
実装優先と同時にプログラムの保守管理を念頭に置くべきでしょう。
以上のことを注意すれば「継承のジレンマ」から脱却できます。
そして、「継承できないの?設計が悪いんだよ」と言う継承主義の人を救ってあげましょう。
ただし、「1.」で言った「頭のフル回転」の理解ができなければ返り討ちにあってしまいますよ。
ここでは、「1.」を克服して更に1歩先に行くことを目的にしています。
戻る