関数で実装したオブジェクトに手続き的に指示を与える、これが理想のプログラミングパラダイムではないかという仮説です。
ただの持論なので学術的背景はありませんが、まさかりよろしくです!
#オブジェクト指向あるある:責務はどっち?
このじめじめした季節、爽快なシューティングゲームを作りたいですよね。おもむろにコードを書きます。
class Player{
num x, y;
}
class Enemy{
num x, y;
}
さて、当たり判定を作る段階に入りました。
しかしここで手が止まります。当たり判定のコードはどちらに書けば??
class Player{
num x, y;
//bool hitTest() ??
}
class Enemy{
num x, y;
//bool hitTest() ??
}
PlayerとEnemyを知らないと当たり判定を行うことはできないわけです。つまり、PlayerとEnemyを知るオブジェクトが行うのが妥当です。
コードを探します…ありました、ゲーム世界全体を表す、Fieldクラスです。
class Player{
num x, y;
//bool hitTest() ??
}
class Enemy{
num x, y;
//bool hitTest() ??
}
class Field{
bool hitTest(); //ここだ!!
}
そしてコードをごりごり実装しました、めでたし、めでたし。
##めでたくありませんでした
ほんとうにめでたいでしょうか?hitTestの実装を見てみましょう。
bool hitTest(){
var dx = player.x - enemy.x;
var dy = player.y - enemy.y;
return dx*dx + dy*dy < HIT_SIZE*HIT_SIZE;
}
なんということでしょう、データ構造が露出しています!!オブジェクトがスケルトンカラー状態で、内蔵が見えています。グロいですね。
どうしたらいいでしょう?プロパティにするのは根本的解決になっていません。例えば、空中、地上の概念を後から追加して、場所が異なっていればあたらない、というコードを追加することを考えましょう。
bool hitTest(){
var dx = player.x - enemy.x;
var dy = player.y - enemy.y;
return (dx*dx + dy*dy < HIT_SIZE*HIT_SIZE) && (player.z == enemy.z);
}
Player、Enemyのデータ構造が、Fieldの実装に影響を及ぼしています。仮にプロパティでも同じことです。
オブジェクト指向のメリットの一つは、データ構造を隠蔽して、メッセージ応答にすることで、各オブジェクトの粗結合を促進することです。
これはデータが露出してるせいで、密結合してます。あれあれ?
問題を整理しましょう。hitTestは、PlayerとEnemyが重なってるかどうか、チェックして返します。
そうです。したいことをダイレクトに書けばいいんです。実装なんてなかったんだ!!
bool hitTest(){
return player.isOverlap(enemy);
}
すっきりしました。めでたしめでたし。
##ついに納得のいくコードに!
しかし、責務を押し付けられたかわいそうなPlayerの実装はどうなるでしょうか……
bool isOverlap(Enemy enemy){
var dx = x - enemy.x;
var dy = y - enemy.y;
return (dx*dx + dy*dy < HIT_SIZE*HIT_SIZE) && (player.z == enemy.z);
}
どこかで見たようなコードになりましたね。片方の情報が自分になっただけマシですが、依然としてEnemyのデータ構造に依存しています。
というより、もとの問題を全く解決してません(当たり判定コードをどちらに追加するのか)
ここでプログラマの武器、抽象化を振るうときです。
この計算は、レイヤー化された円と円の当たり判定、と見ることができます。
よって以下のクラスが導出できます。
class LayerCircle{
num x, y;
int z;
bool isOverlap(LayerCircle other){
var dx = x - other.x;
var dy = y - other.y;
return (dx*dx + dy*dy < HIT_SIZE*HIT_SIZE) && (player.z == enemy.z);
}
}
そしてPlayer is a LayerCircle, Enemy is a LayerCircleです。
class LayerCircle{
num x, y;
int z;
bool isOverlap(LayerCircle other){
var dx = x - other.x;
var dy = y - other.y;
return (dx*dx + dy*dy < HIT_SIZE*HIT_SIZE) && (player.z == enemy.z);
}
}
class Enemy extends LayerCircle{
}
class Player extends LayerCircle{
}
class Field{
Enemy enemy;
Player player;
bool hitTest(){
return player.isOverlap(enemy);
}
}
できた!!
#ちょっとまって、なんでそんなに難しいの?
実は、説明のため、わざと長い道のりでコーディングしました。
EnemyとPlayerが共通のデータ構造をもつ時点で、共通のBaseが存在することは自明なのです。
ですが、このケースが簡単だった、というだけでしかありません。
例えば、これに矩形の当たり判定をもつクラスを追加してみるとどうでしょう。
変更箇所が多いですよね。これはExpression Problemというよく知られた問題なので、ちょっとずるいかもしれませんが……
Expression Problemは脇に置いておいて、初心に戻ってhitTestの意味を考えてみましょう。
hitTestは当たり判定です。AとBが重なっているかどうかを判定する物です。なんでこれを直接実装できないんでしょう。例えば、こんな風に。
bool hitTest(enemy, player){
var dx = enemy.x - player.x;
var dy = enemy.y - player.y;
return (dx*dx + dy*dy < HIT_SIZE*HIT_SIZE) && (player.z == enemy.z);
}
このコード、円と円の当たり判定アルゴリズムを知っていれば、何を書いているのか明らかですよね。
#オブジェクト指向は抽象度が高すぎる
なんでまどろっこしいことになったのでしょうか。一般的に、問題に対して分割の粒度が小さすぎると、通信コストが増え、複雑になります。
今回のケースが、まさにそうです。当たり判定という問題は、オブジェクト指向言語で組むには小さすぎたのです。
#この粒度にベストなパラダイムは、関数型プログラミング
さて、オブジェクト指向がマッチしないなら何がいいでしょうか。
手続き型でしょうか。そのように見えます。ですが、hitTestはなんら副作用をもちません。
すなわち、hitTestは 参照透過な関数 なのです。
これを上手く扱えるパラダイムは関数型なので、関数型で組むと良いです。
#関数型プログラミング最強!?
関数型プログラミングはすばらしいパラダイムです。ですが、一つ大きな問題が残ります。副作用の扱いです。
純粋関数型言語は、そのすばらしい参照透過世界を維持するため、副作用を外だししました。
...f(f(f(world,d))))...のように、プログラムを外世界を別の外世界に変換する関数と定義したのです。worldに関してはいっさい関知しないことで、f()は純粋なままです。
さらに、遅延評価をデフォルトとすることで、勝手に適用して、世界を終焉まで計算することもありません。
よし、これで関数型世界は守られました。すべては純粋な関数なんだ!めでたしめでたし。
#あれ、オブジェクトだった!?
ここで終わればいいんです。ですが、このプログラム、外から眺めてみましょう。
...f(f(f(world,d))))... これはfを宣言してるにすぎません。遅延評価なわけですから、これを評価するためのきっかけが必要なのです。
では、きっかけは何でしょう?内部にはありません、当然です。ですから外部にあるわけです。
つまり、このプログラムは
- dという他とは違うデータをもち
- 外部の刺激に反応する
なにかなわけです。はい、ここでオブジェクト指向はプログラムをどうやって表現するか書きますね。
- オブジェクトという、アイデンティティをもったものが、
- メッセージを投げ合い、相互作用する
world,d対はアイデンティティをもっています。
そして外部の刺激とはメッセージです。
なんということでしょう、このプログラムはオブジェクトでした!!
#オブジェクトって大きいな
ここまできて、初めて先の問題点がクリアになります。
オブジェクトとは、このくらい大きな粒度なのです。
先の例の場合、EnemyとPlayer、Field、すべてまとまって初めてオブジェクトの粒度となるわけです。
#メッセージパッシングの中に隠れているモノ
さてさて、オブジェクトはかなり大きな単位と言うことがわかりましたが、これが相互作用することでシステムが組めるでしょうか。
可能か不可能かで言えば、可能です。ですが、すべてのオブジェクトがすべてのオブジェクトと通信してしまえば、とたんにカオスです。そこには構造が必要です。
ここでメッセージについて考えてみましょう。理想的には瞬時に伝わります。ですが、現実にはタイムラグがあるわけですよね。タイムラグがあるということは、一定時間、情報を保存している何かがあるのです。
つまり、メッセージパッシングには媒介者が隠れています!!
この媒介者の構造こそが、必要なものです。
#メッセージを時系列で記したもの
いったいメッセージの媒介者は何でしょうか?
神様?いえ、違います。AとBを媒介するには、AとBを知るCが必要ということを意味します。
というわけで、このCの構造を考えるため、中身を実装することを考えます。
メタな世界のままではわかりづらいので、ここは具体例でいきましょう。暑い夏にぴったりなシューティングゲームです。
シューティングゲームはいくつかのステージに分かれます。ステージのアイデンティティは十分に高いように見えまね。なのでステージをオブジェクトとして扱います。
そして、ステージの媒介者を、そうですね、シーンと名付けましょう。
シーンの実装はこうなりそうです。
class GameScene(){
Stage currentStage;
void process(){
if(currentStage.checkClear()){
nextStage();
}else if(currentStage.checkGameOver()){
continueStage();
}
}
}
さて、よく見てみましょう。メッセージを逐次実行、これはまさに 手続き です。
メッセージパッシングの媒介者の実装は、手続き型になります。
#すべての点は線で結ばれた!
ここまでをふまええると、理想のプログラミングはこうなりそうです。
- まずデータを洗い出す
- データに対する処理を純粋な関数として実装する
- 外界へのインタラクションが必要な部分を境界として、関数とデータをオブジェクトとしてまとめる。
- オブジェクト同士を調停するオブジェクトのメソッドは手続きとして書く。
関数型、オブジェクト指向、手続き型が繋がりました!万歳!
#Happy Programming!!
初めは手続きでした、ですが、CPUに対しての手続きだったため、人間が扱うには低レベルすぎました。
そこで抽象度を上げるわけですが、大きく二つの流れに分化しました。
オブジェクト指向プログラミングと、関数型プログラミングです。
しかし、オブジェクト指向プログラミングは問題に対して粒度が大きすぎ、関数型プログラミングは小さすぎました。
そして現在、両者は統合されようとしています。(Java8、Scala,Swift,etc...)
しかし、統合したものの、どこにどう適用するか、言語は答えてくれません。
そして、そのガイドとなるのが、粒度です。
これに気をつければ、すばらしいプログラミング世界が広がっていることでしょう、たぶん!!