導入
Processingで,オブジェクト指向を実現する「クラス」を導入し,より効率的なパッケージングを行う.
オブジェクト指向プログラミングとクラス,オブジェクトインスタンス
オブジェクト指向プログラミングの考え方
「オブジェクト指向プログラミング」とは,モノの中に変数と動作を規定する関数があると想定し,それをパッケージングする手法である.
例えばこれまでの課題のボールに必要な「xとy座標」「xとyの増加方向」「xとyのスピード」「ボールの色」等の変数,跳ね返りのための判定機能は,Processingのメインスケッチ内に配列として定義されていた.
これらの状態を保持する変数と機能を,一つの「ボール」というモノの中に閉じ込め,メインスケッチからは,一つのボール,二つのボール,という風に扱う考え方が,オブジェクト指向プログラミングである.
この考え方を使って書かれたプログラムが以下のコートである.メインスケッチではBallという型の変数とその配列,Ballの中に含まれているdrawBall()
関数,updatePosition()
関数のみを扱えばよい.跳ね返りや速度,色等の情報はBall型の変数内部に隠蔽されて直接扱う必要はなくなる.
クラスとオブジェクトインスタンス
ソースコードBall.pde書かれたclass Ball{}
が「クラス」である.クラスは定義であり,どのような変数が内在しどのような動作をするかの設計図のようなものである.
コード例ではメインスケッチ内で,このBallクラスの配列を10要素分宣言し,new Ball()
している.このnew
キーワードでクラス設計図を基に実際にプログラム内に生成する.生成されたモノ(Ball)はそれぞれ独立して変数を持ち,またメインスケッチ内から関数を呼び出せるようになる.このモノを「オブジェクト」もしくは「インスタンス」と呼ぶ.
また「クラスからオブジェクトを生成する」ことを「オブジェクト化」もしくは「インスタンス化」と呼ぶ.
画像読み込みに使ったPImage型の変数を思い出してみよう.このPImageもクラスであり,そのインスタンスを生成し,内部に画像ファイルを持たせ,描画する命令を出している.
メインスケッチの中では,画像ファイルの中身がどうなっているかにも触れずに,ただ画像を読み込ませ,指定した座標に描画する,という命令を下すだけである.これがオブジェクト指向プログラミングである.
コンストラクタ
Ballクラス内にクラスメイト同じBall()
と名付けられた関数がある.このクラスメイト同名の関数を「コンストラクタ」(Constructor)と呼ぶ.コンストラクタはメインスケッチ内でnewされインスタンス化される時に自動的に呼ばれる関数で,インスタンスの初期化の役を担っている.
設計図であるクラスに書かれた変数は,このコンストラクタ内で初期化(値を入れられる)されることが一般的である.
メンバ変数とメンバ関数 - カプセル化
クラス設計図に含まれる変数のことを「メンバ変数」,含まれる関数のことを「メンバ関数」と呼ぶ.インスタンスのメンバ関数はそのインスタンス内のメンバ変数にはアクセスできるが,メインスケッチからはメンバ関数を通じてしかインスタンス内のメンバ変数に触ることはできない.
この関数だけが表に出て触れることを,オブジェクト指向の「カプセル化」「情報隠蔽」「データ隠蔽」などと呼ぶ.
例えば現実の野球ボールを考えてみる.硬球と軟球の違いで我々に見えるのは,ボールをバウンドさせた時の跳ね方の違い,投げた時,受けた時の感触の違い等,「ボールの挙動」だけで,中身がゴムであるか鉄であるかを見抜くことはできない(極端な話,ゴムみたいな性質を持った新構造の鉄かもしれない).我々にとっては野球ボールが期待するボールの挙動をしていることがわかれば十分であり,ボールが跳ね返る時に内部の鉄分子がどういう動きをしているかなどは推し量る必要もない.
このような内部の変数を考える必要がないことが「カプセル化」「情報隠蔽」「データ隠蔽」である.
(本来ならば)メンバ変数の頭にはprivate
キーワードがつく.(例えばprivate int xPos;
のように宣言する) このprivate
とはそのインスタンスのメンバ関数からしか触れないことを示す.
ただし(これがJavaの欠点なのだが),priavate
をつけてしまうと次項で行う「継承」が行えなくなってしまうため,何もつけない(後述のprotected)のが一般的である.
対してメンバ関数はメインスケッチから呼び出せる必要があるため,private
宣言してしまうとまずいことになる.Javaでは何もキーワードをつけない場合protected
というキーワードがついていると見做される.このprotected
とは同じパッケージ(Processingでは同じスケッチ)からのみ触れることを示す.
またpublic
キーワードをつけると,別のパッケージから呼び出すことができる.(Processingではメインスケッチ以外のクラスを別のスケッチから呼び出すことはないので無意味である)
余談ではあるが,classの宣言そのものにもpublic, protected, privateもつけることができる.この後Swing GUI部品を使うが,これらのGUI部品などJavaのクラスライブラリは(当たり前だが)全てpublic指定されている.
インスタンス関数(インスタンスメソッド)
そのインスタンスのメンバ変数と密接に結びつき,そのインスタンスのメンバ変数しか触らない関数のことをインスタンス関数(インスタンスメソッド)と呼ぶ.
コード例のBallクラスのメンバ関数updatePosition()
,drawBall()
は両方とも,インスタンス内部の変数を取り扱うためインスタンス関数である.
静的関数
オブジェクト指向プログラミングではほぼ全てがインスタンスメソッドであるが,頭にstatic
をつけて宣言された関数は静的関数(staticメソッド)と呼ばれる.この静的関数はインスタンスのメンバ変数を触らずに,入力と出力がクラスからは完全に独立している.
例えばMathクラス https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Math.html の仕様を見てみると,ほぼ静的関数であることがわかる.
Javaの命名規則
- クラスの名前(定義)は語頭が大文字
- 変数,オブジェクト,インスタンスは語頭が小文字
- 2語以上続く場合は2語目以降の語頭が大文字
- 関数名は「動詞+名詞」の形になることがオブジェクト指向プログラミングの考え方の上で自然であり,やはり2語目以上の語頭は大文字
これらの命名規則はJava2(Java ver.1.2)以降に確立したものであり,非常に古いクラスライブラリでは通用しないものが多い.が,そのような古いライブラリは使わないように推奨されている.
コード
タブの右側の下矢印から「新規タブ」.
クラスの名前,ここでは「Ball」とつけて,「OK」.
Ballという名前の新しいソースコードのタブが追加される.
エクスプローラやFinderでプロジェクトの中身を見てみると,「Ball.pde」が追加されている.
//BounceBall.pde
//Ballクラスの配列10要素分を作る
Ball ballArray[] = new Ball[10];
void setup(){
size(640, 480);
colorMode(RGB, 255);
background(255, 255, 255);
frameRate(10);
for(int i=0; i<10; i++){
// ボールクラスからインスタンスを作り,配列に入れる
ballArray[i] = new Ball();
}
}
void draw(){
drawRefreshRect(255, 255, 255, 60);
for(int i=0; i<10; i++){
// 10個のボールそれぞれ,内部に持っている座標を更新する関数を呼ぶ
ballArray[i].updatePosition();
// 10個のボールそれぞれ,内部に持っている座標をもとに描画する関数を呼ぶ
ballArray[i].drawBall();
}
}
void drawRefreshRect(int r, int g, int b, int alpha){
noStroke();
fill(r, g, b, alpha);
rect(0, 0, width, height);
}
//Ball.pde
class Ball{
int x;
int y;
int ballWidth = 20;
int ballHeight = 20;
int xDirection;
int xSpeed;
Ball(){
// コンストラクタ
// メインスケッチでnewされてインスタンス化された際にこの関数が自動的に呼ばれる.
// 基本的にはメンバ変数を初期化するために用いられる.
x = (int)random(width);
y = (int)random(height);
if(random(2) < 1){
xDirection = 1;
}
else{
xDirection = -1;
}
xSpeed = (int)random(5) + 1;
}
void updatePosition(){
// メンバ関数.
// この関数がメインスケッチから呼ばれることでボールの位置が更新される.
x += xSpeed * xDirection;
if(x > width){
xDirection = -1;
}
else if(x < 0){
xDirection = 1;
}
}
void drawBall(){
// やはりメンバ関数.
// この関数がメインスケッチから呼ばれることでボールが描画される.
fill(30, 30, 30);
ellipse(x, y, ballWidth, ballHeight);
}
}