機能をまとめる(クラス②)

2019-10-22

本章のあらすじ

  • クラスで機能もまとめる

クラスのおさらい:「データをまとめる」

前回は、クラスの機能のひとつ「データを集約する」を学びましたね。

クラス導入前:配列地獄
クラス導入後:すっきり!

データはまとまったが、機能はばらばら

粒のデータは集約されてすっきりしましたが、粒を動かす処理・描く処理は依然としてクラスの外側、 draw() ブロック内に書かれています。

void draw() {
  // ...

  for (int i = 0; i < ps.size(); i++) {
    // i 番目の粒を取得
    Particle pi = ps.get(i);
    
    // i 番目の粒を動かす
    pi.x += pi.vx;
    pi.y += pi.vy;
    
    pi.vx += random(-0.1, +0.1);
    pi.vy += random(-0.1, +0.1);
    
    ellipse(pi.x, pi.y, 10, 10);
  }
}

ここでひとつ例として、いい感じの粒のスケッチができたので、他のクラスメイトに使ってもらいたいと思った場合を考えてみましょう。

まず Particle クラスを丸ごと渡せば、必要なデータは揃いそうです。しかし粒を使ってもらうのにはそれだけでは足りません。

粒をどう動かすか、どう描くか、という機能が残っていますね。

粒に関するこれらの機能は、いまは draw() ブロックの中に散らばっています。データと一緒に、散らばっている処理もコピペしてもらう必要があるので、少し手間がかかりそうです。

もし、 Particle クラスに、データだけでなく機能:ここでは動きの処理と描画処理が含まれていたら…? Particle クラスのみをまるっと渡すだけで、クラスメイトに最高の粒を使ってもらうことができるわけです。

データと機能が揃っていると、もっと便利になる。クラスにはこれを実現するための仕組みが用意されています。

クラス:「機能の集約」編

引き続き、パーティクルのスケッチを題材に進めていきましょう。 (→particle

まずは機能を関数として分離する

1ステップ目として、まずは粒を動かす機能、描く機能をそれぞれ関数として分離してみます。

// 指定された粒を動かす関数
void updateParticle(Particle p) {
  p.x += p.vx;
  p.y += p.vy;
    
  p.vx += random(-0.1, +0.1);
  p.vy += random(-0.1, +0.1);
    
  // 寿命を減らす
  p.life -= 0.005;
}

// 指定された粒を描く関数
void renderParticle(Particle p) {
  float al = map(p.life, 1, 0, 255, 0);
  fill(0, al);
  ellipse(p.x, p.y, p.sz, p.sz);
}

機能を切り出したので、 draw() ブロック内のfor文ではこれらの関数を呼び出すだけでよくなり、だいぶシンプルになります。

void draw() {
  // ...

  for (int i = 0; i < ps.size(); i++) {
    // i 番目の粒を取得
    Particle pi = ps.get(i);
    
    // i 番目の粒を動かす
    updateParticle(pi);
    
    // i 番目の粒を描く
    renderParticle(pi);
  }
}

(→particle_func

しかしながら、インスタンスの外側に機能が漏れていることに変わりはありません。

粒を動かす機能はインスタンスの外側にある

機能をクラスに組み込む

この外側にある機能をインスタンスに持たせるのが「インスタンスメソッド」です。まずは粒の動きに関する機能をクラスに移植しながら感覚を掴んでいきましょう。

クラスの波括弧の中で、以下のように update() メソッドを追加します。

class Particle {
  // ...

  // 粒を動かす
  void update() {
    this.x += this.vx;
    this.y += this.vy;
      
    this.vx += random(-0.1, +0.1);
    this.vy += random(-0.1, +0.1);
      
    // 寿命を減らす
    this.life -= 0.005;
  }
} 

機能の呼び出し側はこのように書き換えます。

void draw() {
  // ...

  for (int i = 0; i < ps.size(); i++) {
    // i 番目の粒を取得
    Particle pi = ps.get(i);
    
    // i 番目の粒を動かす
    pi.update();
    
    // i 番目の粒を描く
    renderParticle(pi);
  }
}

メソッド内に this という新たなキーワードが出てきていますが、これはインスタンス自身を表しています。 this キーワードを使うことでデータの参照先がインスタンスそのものになり、余計なパラメーターを指定しなくてよくなるのです。

動かす機能がインスタンスに組み込まれた

同じ要領で、描画処理 renderParticle() も移植してみてください。

(→particle_instance_method

クラスを別ファイルに分離する

長い道のりでしたが、これで粒に関するデータと機能がすべてひとつのクラスにまとまりました!

最後の仕上げとして、Particle クラスを別のファイルとして独立させましょう。まずはスケッチ名の右にある「▼」をクリックして、「新規タブ」をクリック。

出てきたダイアログにファイル名を入力します。クラス名と同じにしましょう。

新たなタブが作成されるので、ここに Particle クラスの定義をまるごと移動します。

以上でファイルとしても独立しました!

クラスメイトに粒を使ってもらいたいときは、このファイルを丸ごと渡すだけでOKになりました。部品としての再利用性が高まるので、他の人と協業するのはもちろん、自分で作った他のスケッチへの組み込みやすさも格段に向上しています。

(→particle_class_file

実はクラスだったものたち:組み込みクラス

ところで、これまで何気なく使ってきた PImage 。これも実はクラスだったのです。おまじないのようにドット . で色の採取機能を使っていましたが、クラスのメソッドを利用していたのですね。

PImage img;

void setup() {
  // ...

  img = loadImage("monalisa.jpg");
}

void mousePressed() {
  // クリックした位置の色を取得
  // PImage クラスの get() を実行している
  color col = img.get(mouseX, mouseY);

  // ...
}

本章のまとめ

  • クラスは、データや処理をまとめるのに便利に使える1
  • 部品としての独立性が高まるので、他の人と協業したり、過去の資産を活用したりするのに便利

  1. この講座では、クラスの「データの集約」と「機能の集約」という2つの側面に着目して解説しましたが、その他にも所有データを隠蔽する「カプセル化」やクラス間に親子関係を持たせる「継承」、関連性を抽象化する「ポリモーフィズム」などさまざまな性質を持っています。

    クラスを使うことの本質は、ポリモーフィズムによる抽象化・責務の分離であるといえますが、この性質が威力を発揮するのはプログラムがある程度複雑になってきてからで、いきなりこれを理解するのは不可能に近いでしょう。

    データや機能を集約するという側面に限定しても十分に有用であることから、まずはこれらの性質から慣れ親しんでいくのがよいステップと考え、このような構成としています。

    ↩︎