RealBasic University

このコラムはStone Table Softwareのオーナーであり、またREALbasic Developerの編集者でもあるMarc Zeedar氏により書かれたものを、著者の許可を得て翻訳したものです。この翻訳はHREM Researchにより提供されています。この日本語版へのご意見はRBU-Jまでご連絡下さい。

URL: http://www.applelinks.com/rbu/086/
INDEXに戻る

OOP University:パート 10

前回は、簡単なお絵かきプログラムを作成するために、オブジェクトの知識を用いました。今回はさらに、ポリモルフィズムといわれる概念の利点を使うことによって、このプロジェクトをSuperDrawへと拡張します。

ポリモルフィズム

あなたはポリモルフィズムという用語を聞いたことがないかもしれませんが、それをすでに使っています!基本的には、それは前回我々が探求した概念で、サブクラスがその親メソッドをオーバーライドできるというものです。オーバーライドがポリモルフィズムを行う実際の方法です;ポリモルフィズム自身は、異なるオブジェクトが、異なる事を実行する同一のメソッドを持つという概念です。

ポリモルフィズムは強力な概念ですが、具体的な例題なしでは、理解することは難しいでしょう。これがSimpleDrawを作成した理由です。shapeClassに追加したdrawメソッドを覚えていますか?後ほど、circleClassrectClass、そしてtriangleClassのshapeClassのサブクラスそれぞれに、異なるdrawメソッドを実装していきます。

ポリモルフィズムでは次のことができるようになります:描画されたオブジェクトが三角形のオブジェクトのときには、triangleClassのdrawメソッドが呼ばれます。しかし、オブジェクトが円のときには、circleClassのdrawメソッドが実行されます。

ポリモルフィズムの使用なくしては、これはまったく機能しません:我々は、それぞれのオブジェクトに異なる「描画」コマンドを作成しなければならず、それから呼び出しのコード(window1のもの)もそれぞれのオブジェクトに適したコードを呼び出さなければなりません。canvas1のpaintイベントが、こんなにも簡単であったことを覚えていますか?

  
for i = 1 to n
objectList(i).draw(g)
next // i

ポリモルフィズムなしでは、次のようにしなければならないでしょう:

  
for i = 1 to n
if objectList(i) isa circleClass then
objectList(i).drawCircle(g)
elseif objectList(i) isa rectClass then
objectList(i).drawRect(g)
elseif objectList(i) isa triangleClass then
objectList(i).drawTriangle(g)
end if
next // i

これでは、自らを描画できるオブジェクトを持っている意味が全くなくなってしまいますね?少数のオブジェクトではそれほど悪くはなさそうですが、フル機能のお絵かきプログラムは何百種類ものオブジェクトを持つので、それらすべてを迅速にプログラムするためにはポリモルフィズムが必要です(次回のレッスンで、この強力で実用的な例題を目にするでしょう)。

カプセル化

その他の重要なOOPの概念は、カプセル化です。以前にも、大まかでしたがそれについてお話しをしました。カプセル化は、外部コードからオブジェクトのコードを分離する概念です。要するに、内部のオブジェクトは外部のことについて、(伝えられたこと以外は)何も分かりません。そして、外部もオブジェクトの内部動作について何も分かりません。

カプセル化の利点は何でしょうか?それは柔軟性です。外部はオブジェクト内部で何が起こっているのか分からないので、オブジェクトの変更は外部コードに影響を与えません。これにも、実用的な例題が必要でしょう。

カプセル化のもっとも一般的な使用法は、オブジェクトのデータ構造の保護です。あなたがゲームを制作しているとしましょう。昔なつかしい「カルメン・サンディエゴはどこにいる?」(非常に詳細なデータを持った、「犯人捜し」ゲーム)と似たものを作るとします。国のオブジェクトでは:各国は名前、人口、面積、国民総生産(GNP)などのプロパティを持ちます。また各国は、各都市のデータに沿った都市リストのような動的な情報を持ちます。各国は、異なる数の都市を持ち、その各都市のデータも変化するので、この情報は動的になります。

最初このゲームを書き始めたときは、各国の都市リストを保持する単純な配列を使うでしょう。それは次のようになります:

  
Custom class cityClass:
cityname as string
population as integer
latitude as string
longitude as string

cityList(0) as cityClass

これはなかなか良さそうで、うまく行きそうです。ゲームを初期化するときは、大量の国と都市のデータをファイルから読み出し、新たな国のオブジェクトを動的に作成し、それを個々の都市リストと固有の都市データで初期化します。

しかし、ゲームを続けていくと、動作がだんだんと遅くなっていくことにすぐに気がつきます。何故でしょう?いろいろ調べてその原因を突き止めると、プログラムが行っている都市の頻繁な検索が原因であることが分かります。ある国には、何千もの都市があります。この都市のデータ構造のデザインのために、ある都市を探すには次のような順次検索をしなければなりません:

  
n = uBound(cityList)
for i = 1 to n
if cityList(i).cityname = findCity then
return i
end if
next // i

このルーチンは、都市のデータとリンクされたcityListにインデックスを返します。万事よさそうですが、順次検索は非効率的です。少数の検索では問題ありませんが、プログラムではこのような検索を数千という規模で、ゲーム中に数多く行います。プログラムが都市のデータを必要とする度に、まずデータ構造のインデックスを検索しなければなりません。一つの遅い検索が顕著になり、プログラム全体の動作が遅くなります。評価版の体験者たちは不平不満を漏らすでしょう。どうしたらよいでしょうか?

簡単です。非効率的なデータ構造を、もっと効率のよいものに変えたらどうでしょう?あなたは自分でインデックス構造を書くこともできるしょう。しかしREALbasicは、処理速度のために自動的にインデックスを最適化した強力な構造であるDictionaryに対応しています。(つまり、Dictionaryはハッシュ・テーブルです。それはデータが独自の方法で組織化されたものなので、Dictionaryはその中に項目が何千何万とあっても、手に入れたいデータを素早く検索することができます。)

Dictionaryは完璧に思えます:単に都市の名前を渡せば、その国の中から探している項目を返してくれます。cityListの定義は次のようになるでしょう:

  
cityList as dictionary

Dictionaryはバリアント型(variant)としてデータを保存するので、内部にはcityClassオブジェクトを含むどんなものでも保存することができます。完璧です!いくつかのプロジェクトでテストをすると、速度の違いには驚くものがあります:Dictionaryメソッドは、どんな場合でも10から150倍は速くなります。

詳細

速いコンピュータでは、我々の議論している時間はとるに足りません:Dictionaryは悪い結果でも0.5msで、同様の5000項目の順次検索では62msです。しかし、この検索を50回行うと、まるまる3秒間掛かります。あるいは、このリストを50,000項目に拡張すると、Dictionaryでは1,384倍も速くなります!

この例を試してみたい場合には、ここからデモプロジェクトをダウンロードできます。いろいろな配列の値で試すために、コード中の配列の値を変更してみて、検索時間に与える影響を見てください。

しかし、Dictionaryのデータ型を配列の代わりに使用するため、国のオブジェクトを変更すると、すぐに困難な問題に突き当たります。プログラムのいたる所で、あなたはcityList配列に直接アクセスしています。するとDictionaryと配列は全く異なるためにプログラムはコンパイルできないで、これらのルーチンは使えなくなります。

最初に適切なOOPの様式で書いたならば、どの外部コードもcityListのデータ構造を理解しないでしょう。その代わりに、getDataFieldsetDataFielddeleteDataFieldcountDataFieldsなどのあなたが与えた所定のルーチンを経由して、cityListと対話するでしょう。あなたがゲームをそのような方法で書いたならば、配列からDictionaryへのデータ構造の変更は、何も問題を引き起こしません:あなたはただ、このようなルーチンを追加、修正、データ削除するように書き換えなければならないだけで、すべての外部コードはそのままの形で残ります。カプセル化がどれほど時間節約になるか分かったでしょう。

次にSuperDrawプロジェクトで、カプセル化の機能をご覧に入れましょう。

SuperDrawの制作

さて、今回SuperDrawを完成させると約束しましたが、あいにく時間がなくなってしまいました。しかし、今回から制作を始め、次の2回のレッスンでこれを完成させましょう。

始めに、SuperDrawでの目的をみることにしましょう。そこでしたいことは、選択、移動、サイズ変更、そしてオブジェクトの削除と描画を可能にすることです。OOP流にそれを行うには、どのようにすればよいでしょうか?

最初に私がこの例題を思いついたとき、作成した既存のオブジェクトにこの機能を追加することは簡単だろうと思っていました。ところが、すぐにつまづいてしまいました。

問題はこうです:私のカスタム・オブジェクトは、本来のREALbasicのコントロールに基づいていないために、refreshイベントのようなイベントを受信する方法を知りません。これは、必要なときに再描画できないことを意味するので、よくありません。

この問題を解決するために、2つの可能性があげられます。一つ目として、単純にcanvasあるいはrectControlに基づいてshapeClassを作成することができるでしょう(canvasrectControlに基づいています)。その方法では、すべてのサブオブジェクトは、我々に必要なイベントを継承します。しかし、これを試しに実行してみると、REALbasicは「クラスからはコントロールを動的に作成できません」とエラーを出力します

それは何を意味するのでしょうか?REALbasicで動的にコントロールを追加するには(RBU Pyramidのカードゲームでそれをしたことを覚えているでしょう)、新規のコントロールを作成する元となるコントロールが、実際のウィンドウに必要になります。クラスからコントロールのインスタンスを直接に作成できません。それができるのは既存のコントロールからのみです。これを説明する一番簡単な方法は、実際のコードを見ることです:

機能しないもの (無効):

  
dim s as myEditFieldClass

// 既存のコントロールではなく、クラスに基づいて
// 新規のコントロールを作成しているので、機能しません。
s = new myEditFieldClass

機能するもの (有効):

  
dim s as myEditFieldClass

// editField1はmyEditFieldClass型で、
// 現在のウィンドウに存在します。
s = new editField1

納得いきましたか?私はこのような方法でうまく行くことを知っていますが、REALbasicがこの制限を課している理由は分かりません。しかし、これがいつか変更されない限りは、このように対処するしかありません。

次に、再描画の問題に対処する2つ目の方法は、paintイベントを受け付ける既存のcanvasオブジェクトを使用し、そのイベントを我々のカスタム・オブジェクトに渡させます。

最初、これは少し複雑なように思えますが、最終的な結果はエレガントで強力です。我々がすることは、我々のカスタムcanvasオブジェクトを作成して、それに描画に関するすべてを教えることです!

SimpleDrawでは、我々はwindow1に描画のデータ構造を保持させていました。SuperDrawでは、カスタムcanvasクラスにその役割をさせます。これは、window1(外部コード)は我々の描画データ構造については何も知る必要がないので、オブジェクト指向とカプセル化の理念によりいっそう適合しています。

それではプログラムを始めましょう。新規クラスをSimpleDrawのプロジェクト(ファイルメニュー、「新規クラス」)に追加してください。新しいクラスをdrawCanvasClassと名付けて、canvasをそのsuperに設定します。

いま我々は、window1からdrawCanvasClassへデータ構造を移動させようとしています。drawCanvasClassを開いて、objectList(0) as shapeClassというプロパティ(編集メニュー、「新規プロパティ」)を追加してください。window1からはプロパティを削除してください。

詳細

REALbasic 4.5の新機能で、項目をコピーするには、あるウィンドウから別のウィンドウに項目をドラッグするときに、オプションキーを押し続けます。window1objectListプロパティからdrawCanvasClassへコピーするのに、この方法を使うことができます。あとは、ただwindow1から削除するだけです!

以前のプログラムでは、描画キャンバスへオブジェクトを追加するコードをaddObjectButton内に持っていました。しかし現在は、我々は特別のキャンバス・オブジェクトを作成しているので、そのコードをdrawCanvasClassに入力します。では、addObjectButtonactionイベント内のすべてのコードを選択して、それをクリップボードにコピーしてください。

新規メソッド(編集メニュー、「メソッドの追加」)を、次のようにdrawCanvasClassに追加してください:

このメソッド内で、以前のコードを貼り付けます。さてここで、このコードを少しだけ修正する必要があります。まず始めに、我々はこのメソッドにサイズ情報を渡しているので、宣言していた変数の大部分は必要なくなります。よって、次のようにdim文は単純になります:

  
dim n as integer

次に、uBoundrndのコード部分を取り除きましょう:我々はこれを違うやり方で行います。我々のコードは、select case文で始まりますが、indexの代わりに、kind(渡されたパラメータの一つ)を調べさせます。

それから、場合分けのobjectList(n) = objectList.appendコマンドに変更します。

続くコードでは、uBound(objectList)としてnを定義する必要があります(appendコマンドのでないと、最新のものになりません) 。そして、オブジェクトのプロパティを、渡されたパラメータに設定します。最後に、我々はキャンバス内にいるため、再描画コマンドを変更します(キャンバス全体は再描画しません。より効率的に、現在のオブジェクトの領域だけです)。

最後に、完成したルーチンは次のようになります:

  
dim n as integer

// Create object
select case kind
case 0 // circle
objectList.append new circleClass
case 1 // rect
objectList.append new rectClass
case 2 // triangle
objectList.append new triangleClass
else
// Just in case
objectList.append new shapeClass
end select

n = uBound(objectList)
objectList(n).left = left
objectList(n).top = top
objectList(n).width = width
objectList(n).height = height

// Force this object to draw
refreshRect(left - 4, top - 4, width + 8, height + 8)

あと数ステップだけで、ほとんど終わりです。

次に、canvas1のpaintイベントのコードをコピーして、それをdrawCanvasClasspaintイベントに入力します。canvas1のpaintイベントからは、そのコードを削除してください。

しかしもちろん、我々はたった今、新規のキャンバスのためにクラスを定義しました:それに何か実行させるには、ウィンドウにそのインスタンスがなければなりません。したがって、canvas1を選択し、そのsupercanvasではなくdrawCanvasClassとしてください。

さて、ここでaddObjectButtonのコードを変更しなければなりません。以前は新規のオブジェクト作成のすべてを行っていましたが、ここではdrawCanvasClassオブジェクトにオブジェクトを追加するよう伝えるだけです!

ここに新しい、もっとシンプルなaddObjectButtonのコードを掲載します:

  
dim l, t, w, h as integer

// Generate random size and location
w = round(rnd * 20) + 10
h = round(rnd * 20) + 10
l = rnd * (canvas1.width - w)
t = rnd * (canvas1.height - h)

canvas1.addObject(index, l, t, w, h)

これでSimpleDraw2(先週のバージョンと区別するためにこう呼びます)は実行可能なはずです。そして、以前とまったく同じ動作をするはずです。

たくさんの仕事をしたのに、始めの状態に戻っただけじゃないかとあなたは考えると思います――ユーザの視点から見ると、我々のプログラムは何も進歩していません!

しかし、水面下では改善されています。window1のコードを見てください。そこには、いまコードが殆ど存在しません!以前のバージョンと比べてみてください。この新しい方法は、以前より簡単で洗練されていないでしょうか?addObjectButtonのコードがシンプルになっただけではなく、addObjectButtonは我々のデータ構造については何も知りません。描画データの保存方法に変更を加えようとするとき、外部コードのいかなる部分にも手を加えずにそれを行うことができます。

ふーっ!今回のレッスンはこれで終わりです。次週では、これらのオブジェクトで本当に素晴らしい何か(それらを選択可能にさせる)をして、きわめて強力かつシンプルな描画環境を整えます。特筆すべき点は、それをOOP様式で作成したために、我々は描画部分を他のプログラムで簡単に再利用できます――私はこれを使った具体的な計画があります!

今週のチュートリアル (リソースを含む)の完全なREALbasicプロジェクトは、ここからダウンロードしてください。

次週

SimpleDrawSuperDrawに変化させます。

Letters

コラムがとても長かったので、今週はお便りコーナーはありません!


RBU-Jの通知サービス!コラムが発表されるたびに日本語版REALbasic Universityのお知らせの emailがあなたに届きます。登録・削除は ここ から。

INDEXに戻る