「ポリモーフィズム」というのは、オブジェクト指向プログラミング言語のもつ性質のひとつです。平たくいえば「見た目は同じ、中身は別人」ということです。たとえば、家の中にはリモコンがいくつかあるでしょう。おそらくそのほとんどに「電源」という名前のボタンがあります。そのボタンを押せば電源が入ります。ただし、TVかビデオか、あるいはエアコンか、どの電化製品が動くかはリモコン次第です。
このポリモーフィズムを活かすと、条件分岐の処理もすっきりと整えられることがあります。一般にはクラスの設計として採上げられるお題を、本稿の前半はフレームアクションで説明してみましょう。クラスについての考え方は、後半で解説します。
01 インスタンスごとに条件分けして異なった動きを与える
タイムラインにMovieClipインスタンスを3つ置いて、それぞれに異なったアニメーションをさせてみましょう。インスタンスの動きは単純に、それぞれ3次元空間で水平と垂直に回し、2次元平面で伸び縮みさせます(図001)。また、ステージをクリックしたら、インスタンスすべてをもとの状態に戻します。
図001■3つのインスタンスに3次元空間の水平・垂直回転と2次元平面の伸縮をさせる
3つのMovieClipインスタンスには、それぞれmy0_mc〜my2_mcという名前をつけておきます(図002)。アニメーションは、DisplayObject.enterFrameイベント(定数Event.ENTER_FRAME)のリスナー関数が扱います。また、ステージのクリックは、Stageオブジェクト(DisplayObject.stageプロパティ)のInteractiveObject.clickイベント(定数MouseEvent.CLICK)にリスナー関数を登録して受取ります。
図002■タイムラインに置いた3つのMovieClipにインスタンス名をつける
まずは、インスタンスごとに条件で切り分けて、それぞれのアニメーションを制御してみます(スクリプト001)。3つのMovieClipインスタンスは、まとめて扱いやすいように配列に入れます(第2行目)。イベントDisplayObject.enterFrameとInteractiveObject.clickのリスナー関数(xMove()とxStop())は、ともにforステートメントで配列に納めたインスタンスをすべて取出し(第11行目〜および第32行目〜)、switchステートメントによりインスタンスを仕分けて処理します(第13行目〜および第34行目〜)[*1]。
スクリプト001■インスタンスごとにswitchステートメントで分けて処理する
// フレームアクション: メインタイムライン
- const DEGREES_TO_RADIANS:Number = Math.PI / 180;
- var mcs_array:Array = [my0_mc, my1_mc, my2_mc];
- var nAngle:Number = 0;
- var nIncrement:Number = 5;
- addEventListener(Event.ENTER_FRAME, xMove);
- stage.addEventListener(MouseEvent.CLICK, xStop);
- function xMove(eventObject:Event):void {
- var nLength:uint = mcs_array.length;
- nAngle += nIncrement;
- nAngle %= 360;
- for (var i:uint = 0; i < nLength; i++) {
- var my_mc:MovieClip = mcs_array[i];
- switch (my_mc) {
- case my0_mc :
- my_mc.rotationY = nAngle;
- break;
- case my1_mc :
- my_mc.rotationX = nAngle;
- break;
- case my2_mc :
- var nScale:Number = Math.cos(nAngle * DEGREES_TO_RADIANS);
- my_mc.scaleX = nScale;
- my_mc.scaleY = nScale;
- break;
- }
- }
- }
- function xStop(eventObject:MouseEvent):void {
- var nLength:uint = mcs_array.length;
- removeEventListener(Event.ENTER_FRAME, xMove);
- stage.removeEventListener(MouseEvent.CLICK, xStop);
- for (var i:uint = 0; i < nLength; i++) {
- var my_mc:MovieClip = mcs_array[i];
- switch (my_mc) {
- case my0_mc :
- my_mc.rotationY = 0;
- break;
- case my1_mc :
- my_mc.rotationX = 0;
- break;
- case my2_mc :
- my_mc.scaleX = 1;
- my_mc.scaleY = 1;
- break;
- }
- }
- }
|
ふたつのリスナー関数におけるswitchステートメントの処理内容は、とくに難しいところはありません。ひとつひとつのインスタンスの扱いは、ごく単純といえます。けれど、この調子で進めていくと、面倒なことになりそうな予感がします。
たとえば、アニメーション(xMove())と停止(xStop())のほかに、画像を切替えるとか、3つのインスタンスに対する別の操作がさらに加わるかもしれません。するとまた、forループで取出したインスタンスをswitchステートメントで切り分ける関数が増えます。あるいは、異なるアニメーションをさせる新たなインスタンスが、出てくることも考えられます。そのときは、それぞれの関数に条件分岐をひとつひとつ書き加える必要があります。そこまでやり終えてから、ひとつのインスタンスの振舞いを変えることになったら、すべての修正箇所を探すのに頭が痛くなりそうです。
[*1] forやswitchステートメントについては、それぞれgihyo.jp連載「ActionScript 3.0で始めるオブジェクト指向スクリプティング」
第23回「クラスのデザインとループ処理」の「ループ処理で複数のインスタンスを生成する」および第14回「キー操作とif以外の条件判定」の「switchステートメント」をお読みください。
|
02 インスタンスに同じ名前の関数を定義して呼出す
それでは、ポリモーフィズムの考え方を採入れてみましょう。前掲スクリプト001で、インスタンスのアニメーションや停止を、それぞれ関数(xMove()やxRotate())としてまとめることは正しいです。ただ、インスタンスの側から見ると、自分に対してどのような処理が行われるのかは、いくつもの関数の中身をひととおり眺めないとつかめません。
そこで、インスタンスの処理は、そのシンボルのフレームアクションに関数としてまとめます。そうすると、今度はメインタイムラインからは、それぞれのインスタンスごとに別々の関数を呼出さなければなりません。さてここで、ポリモーフィズムです。インスタンスそれぞれの異なった関数に、すべて同じ名前をつけてしまいます。アニメーションさせるのはxMove()、その停止はxStop()と、リスナー関数と同じ関数名にしましょう(スクリプト002)。これでリスナー関数は、インスタンスすべてに対して、同じ名前の関数を呼出せば済みます(第12および21行目)。
スクリプト002■インスタンスすべてに対して同じ名前の関数を呼出す
// フレームアクション: メインタイムライン
- var mcs_array:Array = [my0_mc, my1_mc, my2_mc];
- var nAngle:Number = 0;
- var nIncrement:Number = 5;
- addEventListener(Event.ENTER_FRAME, xMove);
- stage.addEventListener(MouseEvent.CLICK, xStop);
- function xMove(eventObject:Event):void {
- var nLength:uint = mcs_array.length;
- nAngle += nIncrement;
- nAngle %= 360;
- for (var i:uint = 0; i < nLength; i++) {
- var my_mc:MovieClip = mcs_array[i];
- my_mc.xMove(nAngle);
- }
- }
- function xStop(eventObject:MouseEvent):void {
- var nLength:uint = mcs_array.length;
- removeEventListener(Event.ENTER_FRAME, xMove);
- stage.removeEventListener(MouseEvent.CLICK, xStop);
- for (var i:uint = 0; i < nLength; i++) {
- var my_mc:MovieClip = mcs_array[i];
- my_mc.xStop();
- }
- }
|
ふたつのリスナー関数xMove()とxStop()は、ともにforループで配列に納められたインスタンスすべてを取出し、それぞれに対してリスナー関数と同名の関数を呼出しています(スクリプト002第10〜13、および第19〜22行目)。条件判定がなくなって、すっきりとしました。
あとはそれぞれのインスタンスのシンボルに、フレームアクションとしてリスナー関数から呼出される関数xMove()とxStop()を定義すればよいです(スクリプト003〜005)。なお、前掲スクリプト001第1行目で宣言していた定数(DEGREES_TO_RADIANS)は、その値を用いるシンボルのフレームアクション(スクリプト005第1行目)に移しました。
スクリプト003■インスタンスを3次元空間で水平に回す
// フレームアクション: インスタンスmy0_mcのシンボル
- function xMove(nAngle:Number):void {
- rotationY = nAngle;
- }
- function xStop():void {
- rotationY = 0;
- }
|
スクリプト004■インスタンスを3次元空間で垂直に回す
// フレームアクション: インスタンスmy1_mcのシンボル
- function xMove(nAngle:Number):void {
- rotationX = nAngle;
- }
- function xStop():void {
- rotationX = 0;
- }
|
スクリプト005■インスタンスを2次元平面で伸び縮みさせる
// フレームアクション: インスタンスmy2_mcのシンボル
- const DEGREES_TO_RADIANS:Number = Math.PI / 180;
- function xMove(nAngle:Number):void {
- var nScale:Number = Math.cos(nAngle * DEGREES_TO_RADIANS);
- scaleX = nScale;
- scaleY = nScale;
- }
- function xStop():void {
- scaleX = 1;
- scaleY = 1;
- }
|
これで、それぞれのインスタンスの動きは、そのシンボルのフレームアクションを見れば把握できます。あるインスタンスの振舞いを変えるのも難しくないでしょう。新たなアニメーションのインスタンスを加えるときは、そのフレームアクションに決まった関数(xMove()とxStop())を定義して、インスタンスの配列(mcs_array)に加えるだけで済みます。
03 インターフェイスとクラスを定義する
前掲スクリプト003〜005のフレームアクションをクラスとして定義し直します。[ライブラリ]に納められているmy0_mc〜my2_mcのシンボル(Clip0〜Clip2)には、クラスとしてClip0〜Clip2を設定しておきます(図003)。なお、MovieClipシンボルに対するクラスの定義について詳しくは、gihyo.jp連載「ActionScript 3.0で始めるオブジェクト指向スクリプティング」 第22回「MovieClipシンボルにクラスを定義する」をお読みください。
図003■[ライブラリ]のMovieClipシンボルにクラスを設定する
たとえば、前掲スクリプト003をつぎのようなクラスClip0として定義すれば、前掲のメインタイムラインのフレームアクション(スクリプト002)で正しく動かすことができます[*2]。ただ、これではクラスを使うことのもち味が、十分に活かされていません。
package {
import flash.display.MovieClip;
public class Clip0 extends MovieClip {
public function Clip0() {}
public function xMove(nAngle:Number):void {
rotationY = nAngle;
}
public function xStop():void {
rotationY = 0;
}
}
}
|
3つのインスタンスmy0_mc〜my2_mcは、みな決まった関数をもち、フレームアクションから同じように扱えます。そのことをクラスとして明らかにしましょう。
つまり第1に、3つのクラスに決まった関数(xMove()とxStop()))が備わっていることを保証します。第2に、3つのインスタンスを同じデータ型として扱えるようにします。すると今回の例では、インスタンスを配列でなくVectorオブジェクトに納められます。また場合によっては、インスタンスを処理する関数に引数として、同じデータ型で渡せます。
こうした期待に応えるのが、インターフェイスです。インターフェイスは、それを実装するクラスが備えなければならないメソッドを宣言します。また、インスタンスはインターフェイスで型指定することができます。クラス(Clip0〜Clip2)が実装するインターフェイス(IClip)をつぎのように定義しました(スクリプト006)。
スクリプト006■インターフェイスで備えるべきメソッドを宣言する
// インターフェイス定義ファイル: IClip.as
- package {
- public interface IClip {
- function xMove(nAngle:Number):void
- function xStop():void
- }
- }
|
3つのクラス(Clip0〜Clip2)は、いずれもSpriteクラスを継承し、インターフェイスIClipを実装します(スクリプト007〜009。なお、Spriteクラスの継承について後述注[*2]参照)。3つのクラスのインスタンスはインターフェイスIClipでデータ型を指定でき、実装されているふたつのメソッド(xMove()とxStop())が呼出せます。
スクリプト007■インスタンスを3次元空間で水平に回すクラス
// クラス定義ファイル: Clip0.as
- package {
- import flash.display.Sprite;
- public class Clip0 extends Sprite implements IClip {
- public function Clip0() {}
- public function xMove(nAngle:Number):void {
- rotationY = nAngle;
- }
- public function xStop():void {
- rotationY = 0;
- }
- }
- }
|
スクリプト008■インスタンスを3次元空間で垂直に回すクラス
// クラス定義ファイル: Clip1.as
- package {
- import flash.display.Sprite;
- public class Clip1 extends Sprite implements IClip {
- public function Clip1() {}
- public function xMove(nAngle:Number):void {
- rotationX = nAngle;
- }
- public function xStop():void {
- rotationX = 0;
- }
- }
- }
|
スクリプト009■インスタンスを2次元平面で伸び縮みさせるクラス
// クラス定義ファイル: Clip2.as
- package {
- import flash.display.Sprite;
- public class Clip2 extends Sprite implements IClip {
- private const DEGREES_TO_RADIANS:Number = Math.PI / 180;
- public function Clip2() {}
- public function xMove(nAngle:Number):void {
- var nScale:Number = Math.cos(nAngle * DEGREES_TO_RADIANS);
- scaleX = nScale;
- scaleY = nScale;
- }
- public function xStop():void {
- scaleX = 1;
- scaleY = 1;
- }
- }
- }
|
前述スクリプト002のフレームアクションは、以下のスクリプト010のように書替えます。第1に、3つのインスタンスは配列でなく、IClipをベース型とするVectorオブジェクトに納めました(第1行目)。第2に、ふたつのリスナー関数のforループの処理で、Vectorオブジェクトから取出したインスタンスはIClip型の変数に取出して、実装されたメソッドを呼出しています(第11〜12行目および第20〜21行目)。
スクリプト010■インスタンスすべてに対してインターフェイスが実装するメソッドを呼出す
// フレームアクション: メインタイムライン
- var instances:Vector.<IClip> = new <IClip>[my0_mc, my1_mc, my2_mc];
- var nAngle:Number = 0;
- var nIncrement:Number = 5;
- addEventListener(Event.ENTER_FRAME, xMove);
- stage.addEventListener(MouseEvent.CLICK, xStop);
- function xMove(eventObject:Event):void {
- var nLength:uint = instances.length;
- nAngle += nIncrement;
- nAngle %= 360;
- for (var i:uint = 0; i < nLength; i++) {
- var my_mc:IClip = instances[i];
- my_mc.xMove(nAngle);
- }
- }
- function xStop(eventObject:MouseEvent):void {
- var nLength:uint = instances.length;
- removeEventListener(Event.ENTER_FRAME, xMove);
- stage.removeEventListener(MouseEvent.CLICK, xStop);
- for (var i:uint = 0; i < nLength; i++) {
- var my_mc:IClip = instances[i];
- my_mc.xStop();
- }
- }
|
ポリモーフィズムを使うことにより、条件の分岐処理はなくしてしまえました[*3]。大事な点はふたつです。第1に、インターフェイスを実装すると、異なるクラスが同じデータ型として扱え、しかも同じメソッドを備えさせられるということです。しかも第2に、実際には違うクラスですから、実装したメソッドの処理はクラスに応じて定められます。これにより、異なるクラスのインスタンスを同じように扱いながら、クラスごとに処理が定められ、修正や拡張もしやすくなるのです。
[*2] 本文で例として示したクラスClip0は、MovieClipクラスを継承したので前掲スクリプト002のフレームアクションで「正しく動かす」ことができます。MovieClipはダイナミックなクラスなので、前掲スクリプト002第11〜12行目のようにMovieClip型で指定した変数に取出して、Clip0クラスに定義したメソッド(xMove)を呼出せるからです。
このクラスClip0を以下のようにSpriteのサブクラスにすると、Spriteで型指定した変数に対してClip0クラスのメソッドを呼出せば、コンパイルエラーが起こります(図004)。
package {
import flash.display.Sprite;
public class Clip0 extends Sprite {
public function Clip0() {}
public function xMove(nAngle:Number):void {
rotationY = nAngle;
}
public function xStop():void {
rotationY = 0;
}
}
}
|
図004■インスタンスをSprite型で取出すとクラスのメソッドが呼出せない
スクリプト006のインターフェイスIClipを実装することにより、3つのクラスClip0〜Clip1のインスタンスを同じIClip型で指定した変数に入れて、IClipに宣言したメソッドが呼出せるのです(前掲スクリプト010参照)。
[*3] ポリモーフィズムについてWebでは、Javaなどのプログラミング言語で解説されることが多いようです。オブジェクト思考「ポリモーフィズムってなんだ?」、Sacrificed & Exploited「ポリモーフィズムを使ったリファクタリングの実践例」、トルネード・プログラミング「ポリモーフィズムによる条件分岐の排除」などが参考になるでしょう。
|
作成者: 野中文雄
作成日: 2011年2月5日