PPPUC++#16
14. Graphics Class Design
"Functional, durable, beautiful." -- Vitruvius
建築物が持つべき特徴として。原文は"firmitas, utilitas, venustas"で、solid, useful, beautiful (堅固、有用、美しい)ということらしいです。
グラフィックスを扱ってる一連の章の目的は2つです。
- それ自身が、「ちょっと役に立つ可視化手段」
- GUIクラスの実例を通して、「設計&実装技術を俯瞰したい」
本章でやっと継承とかに入ります。というわけで、またしても言語の詳細に立ち入ります。象牙の塔に座してるだけではダメ。設計と実装はバラバラに議論はできないのですよー、というご意見。
14.1 Design principle
- えー、本グラフィックスI/Fクラスの設計原理はー
- なにそれ食べられるの?
- というところから始めましょ
14.1.1 Types
- グラフィックス、と言う応用の例を通して、どうやって概念や機構を作って行くか、を俯瞰
- この部分が大混乱状態だったり、一貫性が無かったり、抜けがあったり、すると大変使いにくくなります。
- せっかく概念的には良くても、上手くコードで表現できていなかったりしても同じです
- 理想的には 概念<ー>コード が一対一に直接対応している状態にもっていきたい
- そうなっていれば、概念を理解すればコードも理解でき、逆もまた然りとなりますすばらしい
- このbookguiライブラリでの概念の例
- Window
- Line
- Point
- Color
- Shape
- てな感じ。一対一対応してます
- これらインターフェイスクラスが集まってライブラリを構成
- ×:バラバラに独立に動作 ○:組み合わせて動作
- こういった方針は新しいクラスを作る際の例示にもなります
- 無関係なクラスをごっちゃごちゃに突っ込んでるわけではありません。ので、設計に関する判断は各クラス独立というわけにはいきません
- クラスは対象を抽象化する視点です。応用(この場合グラフィックス)をどう捉えるか、という指標&視点になります
- すっきり、一貫性、が重要です。視点がぶれてたらロクに見えないのです
- 完全網羅は無理。
- シンプル & 拡張性!
- 実際問題どんなクラスライブラリも問題領域を全マッピングしよう、だなんて無茶な事はしていません
-
- そういう事しようとすると PL-Iになるのですね
- そういう発想は無理、なだけでなく無意味です
-
- ここでは地図情報の例で、「全記号用意すんの? でもそれって余程完璧な地図ソフトでしかいらないし、しかも他の分野にはもっていけないよ?」 という話
- と、言う訳で、まいどまいどの、「重要なのは何?」という方向で
- 全てをやろうとするのは "recipe for failure." (失敗の秘訣だ、くらいかな)
- 良いライブラリというは、問題領域を特定の視点からに限定し、特定部分に注目し強調しているもん、とのこと
- 逆に不要な部分を隠蔽している、と
- 確かに。ライブラリと言うのは実は可能性を高めるものではなくて、むしろ限定する事で「可能性無限大のなんでもできるはずなんだけど、どうしていいのかわからず、なにもできないカオス」から「具体的に何かできる形式」に持っていきやすくする道具なのですよね。拳法の形といっしょ
- 逆に不要な部分を隠蔽している、と
- bookguiライブラリ → シンプルなグラフィックスとGUI部品をターゲット
- 数値演算・科学技術演算などがターゲット
- 機能が足りなければ、このライブラリの上に被せることも、また生FLTKで書くことも可能にしてあります
- 要するにベアで見えてるとw
- まあしかしそれをやるのは 17,18章(ポインタとかメモリ管理)まで待ってね、とのこと
- 設計基準のひとつとして、「多数の小さいクラス/少なめの操作」
- たとえば今回の例では、Polylineクラスを一つだけ用意して何でも屋にするのではなく、Open_Polyline, Closed_Polyline, Mark, Lines, などなど、小さく別のクラスに分けています
- なんでも便利屋クラス、という方針は極論すれば「全てをShape一個だけで表現」となって全く持ってクラスに分ける意味が無くなります
- 更に極論すれば、Universal クラス一つだけでプログラム全体を・・・(ry
- この方針では、さらに悪いことに・・・
- データや操作をユーザが具体的にバリバリ書く必要があって、しかもフレームワーク(すなわち例となる考え方)となるものも無いので、理解も大変、デバッグも大変、最適化も大変です
- 複雑さをいなすためにある抽象化機構なんだから、態々抽象を捨ててユーザが具象判別をやってはだめよね
14.1.2 Operations
- 必要最小限のメソッド(操作)
-
- MECEです
- 便利関数はクラス外のほうが良い
- 理由は前出でしたね
-
- 共通スタイル重要
- 似たような処理→同じ名前
- 似たような引数→同じ型
- 似たような呼び出し→同じ引数の順序
- たとえばbookguiライブラリでは(場所の指定が必要なクラスの場合)コンストラクタの第一引数は必ず場所を示すPointになっています
Line Ln(Point(100,200), Point(300,400)); Mark m(Point(100,200), 'x'); Circle(Point(200,200), 200);
- 点を表すときは必ずPointクラス
- 当たり前、と思うかもしれないけれど、多くのライブラリは(気を利かせすぎて)複数スタイルをごっちゃにしてるものが多いです
- 実は結構問題。たとえば
void draw_rectangle(Point p1(100,200), 300,400); void draw_rectangle(100, 200, 300, 400);
-
- 上の例: 「(100,200) からサイズ300x400で矩形を描く」 と、と容易に類推できるけれど、下の例だと果たして 「300x400」なのか「(100,200)から(300,400)を対角線とする矩形」なのか一瞬では判別つかない
- あるある
- 抽象度が高い、というのは、具体的な数値が何かに包まれて別の概念になっているので、間違いがわかりやすいのですね
- 具体的な数値は可能性無限大で何を示しているのかわかりにくいので、抽象化によって可能性を削ってやってるわけかなるほど
- 上の例: 「(100,200) からサイズ300x400で矩形を描く」 と、と容易に類推できるけれど、下の例だと果たして 「300x400」なのか「(100,200)から(300,400)を対角線とする矩形」なのか一瞬では判別つかない
- これらは実行時の勘違いエラーを劇的に減らす手段だったり
- 論理的に同じ操作には同じ名前
- 理解が容易。新しいクラス作るときも容易
- もっとgenericに、ついては19-21章にて
14.1.3 Naming
- 同じような操作は(ry
- WindowにShapeを追加するのはattach()で, ShapeにLineを追加するのは add()だけど、これって同じ名前でいいんじゃ?
- NO, 根本的に違う
- attachはコネクションを張るだけ。
- pass-by-reference
- Shapeの所有者はcaller(大抵はメインスレッド)のまま. 引数は実際の実在、存在
- pass-by-reference
- addは提示されたオブジェクトをコピーして自分で保持する
- pass-by-value
- Lineの所有者はCallee(Shapeオブジェクト). 引数は点を「指定」するためだけの一時的なモノ
- pass-by-value
- もちろん両方ともadd()にもできるけどモデルが変わってしまう
-
- あとからまた出てくるけど、メインルーチンで持ってるオブジェクトの状態を変化させても(add()でコピーされたオブジェクトは変わらないので)画面が変化しない、という非直感的かつ、アニメーションしようとすると、毎回 deleteして add()し直すと言う面倒I/Fになってしまいます
-
- まあそうはなっていないので、Windowはattachするだけ。Shapeの実体は持っていません
- (赤)ここでオブジェクトの生存期間、というのが重要になってきます
- 関数内部で定義されたローカルオブジェクトの生存期間はその関数の中だけ、、、というやつ
- したがって 下請け関数内部でShapeを作って, それを Windowにattachしてしまうと、関数を抜けた瞬間にWindowが挿しているオブジェクトも捨てられてしまうよー、ということです
- (注)例では f(Simple_window& w) としてWindowを受取っているのだけれど、これを const としておくと(attach()がconst関数じゃないので)エラーで弾いてくれるからイイよ! とか
14.1.4 Mutability
- クラスデザイン時のキーワード:「誰がいじる?」、「どうやって?」
- 状態を変えてるのはちゃんと自分クラスかどうか?
- public/private/protectedはこれをちゃんとするための仕組み
- 構築後、メンバを変更することは可能? どうやって?
- クラス外から、メンバの値をリード可能? どうやって?
- bookguiではほとんどのメンバ変数に直接アクセスは不能になっています
- このメリットはチェックコードを簡単につけられることと、変更に強いこと
- しかしチェックはやってない、とw
- チェックして無い理由は説明を簡単にするためと、エラー値を入力するとどうなるか見てもらうため(データが吹っ飛んだりはしないよ!と)
- と、いいつつ明示的にそんな例は無かったり。自主的にやってみてNE!
- このメリットはチェックコードを簡単につけられることと、変更に強いこと
14.2 Shape
- Windowに現れる「何か」
- Shape -> Window -> [Operating System] ( -> [Physical Screen] ) のマッピング
- Color & Line_Style
- 点集合&どう描くかの知識
- 一般性という観点からは、概念が「3つも!?」だめじゃん, 再利用しにくいじゃん
- 専用設計 →良い設計化 →一般化 →細かい部品多数 →簡単なことが複雑に →悪い設計 あれれれれ?
- 今回の目的は教育目的なので、かえってややこしくならないようにこの辺りで手を打ちましょう
- (で、Shapeの全体が掲載されてる。興味のある人はMr.Sのページから)
- そこそこの複雑さ。いろんなShapeに対応するためだから仕方ない
- とは言え、4データメンバ 15関数程度。各関数は充分単純だし、たいしたこと無い、とか。
- 以降、一つ一つやっていきましょー
14.2.1 An abstract class
14.2.2 Access control
- メンバ変数は全て private:にしてる
- アクセサ関数が必要. いろんな方法が
- 具体的にはXというメンバに対してX(), set_X()
- なんというobjective-C
- 具体的にはXというメンバに対してX(), set_X()
- 一つ問題があって、この方法だとメンバ変数と同じ名前のメンバ関数が使えないこと
-
- うーんダサい
-
- public I/Fに判りやすい名前を。private名は犠牲にしても大丈夫
-
- むーんダサい
-
- Shapeは Pointのvectorをもっています。各形状はこれを使って描画します
- これに点を追加するのがadd()
- 今回はとにかく全てのユーザからデータメンバを隠蔽してます
- ユーザだけでなくShapeの継承した子クラスからも直接アクセスは禁止になってます
- 全てが関数I/Fなのは賛否両論あると思います
- Circle、Polygonといった子クラスは点の意味(図形の描画方法)を知っています。
- Shapeは点の意味を知りません
- 結果として子クラスは点の追加方法についてもちゃちゃをいれられるようにする必要があります
- Circleなら点の追加は無意味なので禁止。Linesなら対になった点じゃないとダメ。Polygonなら交差しない点じゃないとダメ、、などなど
- これが add()が protectedな理由ですね
- 同様の理由でset_point()もprotected
- これが必要なクラスは現時点ではないのですが
- 点の意味を知っているオブジェクトしか点を追加したり、変更したり出来ない、そうじゃないと不変条件が守れない、というのがセオリー
- privateにするのは不用意な変更からメンバを守るためなんだけど、子クラスからは点をアクセスするための便利関数が必要なのでそれは用意されてます
void Shape::setpiotn(int i, Point p) // not used; not necessary so far { points[i] = p; } Point Shape::point(int i) const { return points[i]; } int Shape::number_of_point() const { return points.size(); }
- 関数I/Fで安全はわかったけど、次は当然ながら、効率どうなの?となる
- オーバーヘッドあるんじゃ? サイズ増えるんじゃ?
- NO。宣言部あるのでinline化されるというお話。
-
- -Oつけてないとその限りじゃない気もするけど・・・
-
- さて、こないにガチガチに固める必要性がどこにあるのか、という話ですが
- 実際短く書こうと思ったら privateとか publicとか protectedとか全然考えずにもっと短く12行も短くなる
- まずは、不変条件を「言明」できて、それをコンパイラが守ってくれるので、(予想外のことが起こらない、という意味で)設計が楽になる
- 機能追加など容易。たとえば Fl_Colorを直接出してしまうと「透明」を扱うことができない、など
- 記述も楽になる。I/F関数なら obj.op()なところを、クラスのメンバオブジェクトを直接いじらせると obj.inner_obj.op() となるため.
14.2.3 Drawing shapes
- 肝心な心臓部!
- Shapeの役割は描画。形状描画ルーチンなきゃ意味なしです
- draw()、draw_lines()に分かれてますが draw()は 色とかスタイルを考慮して実際に仕事をするdraw_lines()を呼ぶわけですね
- 正確にはdraw()は前の色やスタイルを戻す仕事も請け負っています
void Shape::draw() const { Fl_Color oldc = fl_color(); // there is no good portable way of retrieving the current style fl_color(lcolor.as_int()); fl_line_style(ls.style(), ls.width()); draw_lines(); fl_color(oldc); fl_style(0); }
- Shape::draw()は fill_colorやvisibilityについては何もしていません
- これを決定するのは実際の描画を知っている個々のクラスになります
- draw_lines()の実現方法
- というわけで、描画についてはShapeの各派生クラスに全件委任
- たとえば、Circle。これは中心と半径が与えられれば、あとは円を書くアルゴリズムをグラフィックスシステムが知っています
- Shapeが「点ベース」という概念を押し付けると「沢山の線分」で円を表すことになってしまいます
- たとえば、Circle。これは中心と半径が与えられれば、あとは円を書くアルゴリズムをグラフィックスシステムが知っています
- ここでついでにvirtualの説明。
- virtualは基底クラスにつけるのです。これでvtableが生成されてpolymorphするようになります
- ありがち過ぎるのですが、これで Shapeのdraw_lines()を呼ぶとちゃんと対応する派生クラスのdraw_lines()になりますよ、という話
- システムは -> Windowを知っていて、 Windowは -> Shapeを知っていて そのdraw()を呼び出せる。各Shapeの派生クラスは書き方draw_lines()を知っていて、、、という知ってるグラフが書ける。
- クラス図、というかresponsibility関係ですね
- move()もdraw_lines()同様にvirtualってるけど、実はいらねーとか
- virtualの例として一応置いといた、とか言ってます
- 一通り変わり得る内部変数を変更する術は担保しておいた方が落ち着きますですはい
- virtualの例として一応置いといた、とか言ってます
14.2.4 Copying and mutability
- コピーガード!
- コピーコンストラクタとコピー代入をprivateにするとか
- まあ定番ですね
- なんでそんなめんどーなことすんの?
- 変にコピーできると 派生クラスのオブジェクト(Circleとか)が Shape扱いでコピーできてしまうから
- Shape部分だけコピー(slicingと呼ぶそうです)された中途半端オブジェクトになってしまう。結果は大災害一歩手前ですね
- そんならそう言ったref->actとかの変換がコントロールできれば良いだけな気も
- "Basically, class hierarchies plus pass-by-reference and default copying do not mix." うーむ???
- 今回のように「クラス階層を構成し、参照呼び出しを使うようなライブラリ」と「デフォルトコピー」は混ぜちゃダメ、ということかな
- ややこしいけど、ようするに「インターフェイスクラスはコピー禁止」を上に継承(w)した感じかな
- 今回のように「クラス階層を構成し、参照呼び出しを使うようなライブラリ」と「デフォルトコピー」は混ぜちゃダメ、ということかな
- コピー用途に明示的に別の関数を用意すべしという話。clone()みたいな感じ
-
- ちゃんと意識してやれってことすね...うーむ
-
14.3 Base and derived classes
- 久々の言語コーナー or 設計ネタ
- このライブラリで使っている言語の機能は3つ
- オブジェクト指向の基礎概念!
- クラス依存グラフがでてます。派生クラス→ベースクラス方向
- 16クラス & Open_polyline 除くと一段の継承
- 商用フレームワーク何かに比べるとベリー小さい
14.3.1 Object layout
- メモリ上ではどうなってんの?
- データメンバは宣言順にデータが並んでいるだけ
- 継承するとその後ろに追加されるだけ
- 仮想関数はvptr, vtblで実現されています
- vtbl は仮想関数のアドレスを順に並べたテーブル。クラスごとに用意する
- 派生クラスでオーバーライドされていない関数はベースクラスのアドレスが入ります
- vptr はそのvtblを保持するデータメンバ
- 仮想関数コール:コンパイラはオブジェクトのvptrからvtblを得て、対応する関数のスロットをリード、そのアドレスをコール、というコードを吐きます
- vtbl は仮想関数のアドレスを順に並べたテーブル。クラスごとに用意する
- というわけで 2 リード + 通常の関数コールのコストがかかります
-
- と、言ってますが、実際には予測失敗(連続の場合はBTBにある)した場合と、間接のペナルティがプラスな気がします
-
- vtblとかプログラミングに必要なの?
- NO, しかし仕組みって気になるだろうし、知らないと簡単に迷信に騙される結果になります
- プロは「なぜか?」「コストは正確にはどれだけか」「何と比べてコスト高?」「コストが問題になるのはどのあたり」などを言える必要がある、というのがMr.Sの意見
14.3.2 Deriving classes and defining virtual functions
- struct A : B {..} と class A : public {..} 。どっちがいいの?
- などという不毛な議論は止めましょう
- しかし class A : B { .. } とすると private継承になってしまうので注意
-
- っていうか、なぜ一番使わないヤツがデフォルトなのか、センスを疑ってし(ry
-
- あと virtual キーワードはクラス外の定義に書かなくてもいいし、書いちゃダメ
14.3.3 Overriding
- オーバーライドは 名前だけでなく、返り値、引数の型、数、const性、など含めてすべて一致していないと起こらないので、ハマるよ、という話をややこしい例でしめしてる面倒くさい節だったり・・・
14.3.4 Access
- メンバのpublic, private, protected に加えて 継承の public, private, protected の話。
- private : public, protected メンバが派生クラスから使用可能
- protected : public, protected メンバが派生クラスと、そこから派生したクラスから使用可能
- public : あらゆる関数から使用可能
14.4 Benefits of object-oriented programming
- OOをつかうとなにがええねんの、の節
- インタフェイス継承をちゃんと守らない設計はボロい。NEVER do that! とのこと
- グラフィックスライブラリの例では Shape::draw()を呼びさえすれば、あとは自動的に必要な処理を実行時に選んでくれるので、大変にインターフェイス継承の有り難味を享受しているのです
- さらに Shape は実際の描画を知らない(知らなくてよい)、ということは、新しい形状を追加しても Shapeのリコンパイルが不要だと言うメリットも
- そううまくいく場合だけでは無いのだけれど、強力なツールです
- 実装継承も便利ですが、もちろん万能薬ではなく、うまくすれば労力削減になりますが、そのために派生クラスがベースクラスにべったりになればなるほどShapeの変更に対して脆弱になります
- 例えばデータメンバの配置を変えてしまうと、すべての派生クラスを際コンパイルする必要が・・・
- 動的重要!
- 例えばデータメンバの配置を変えてしまうと、すべての派生クラスを際コンパイルする必要が・・・