2013年1月10日木曜日

「オブジェクト指向」は、忘れるための技術です

流石に2年も経つと、staticおじさんもオブジェクト指向を概ね理解してきたみたいですね。
http://wonderfulsky.web.fc2.com/oop.html

● メッセージバッシングの多用はプログラムの動作が追いにくい原因となる。関数でできるものは関数で実現するのがよいのではないか?
● デザインパターン本では派生クラスを多用しているサンプルプログラムが多いが、実際の開発でこのようなケースが有効につかえるのか?

業務アプリしか作ったことのない人の意見だなー。
今時のソーシャルゲームなら、幾らでもある話ですよ。
  • 普通はプレイヤーキャラが、モンスターを倒す。
  • しかし、プレイヤー同士が戦うこともある。
こういう場合、いずれのケースとしても(PHPっぽく書くなら)
$player->attack($enemy)

と記述できた方が遥にメンテナンスしやすい筈です。

何故って、このまま右辺と左辺を入れ替えれば、先攻後攻が入れ替わるわけですよ。「static関数の引数の、2番めと3番めが入れ替わってる」より遥に直感的だと思うのですが。

  • $player->attack($enemy);
  • $enemy->attack($player);
勿論、内部的には、攻撃力オブジェクトを、防御力オブジェクトに突っ込んで初めてダメージが出る訳です。モンスターやプレイヤーは恐らく次のような感じで定義するでしょう。
  • interface BattlePerson { function attack( BattlePerson & $target ); 他にも色々 }
  • class PlayerPerson implements BattlePerson { 省略 }
  • class MonsterPerson implements BattlePerson { 省略 }
この2通りだけだったら、「staticで充分だ!」と言うかもしれません。違います。こんなもんじゃないです。

チーム戦だったらどうします? 下手すると、3重ループぐらいに成りかねない。要するに、オブジェクト自身がどうこう、って言うんじゃなくて、呼出側が複雑化するわけです。

そうそう、ゲームは、攻撃力計算にも、バイアスやら倍率やら色々掛かるんですよ。防御力に同じく。おまけになんかアイテム持っていたら、必殺技が発動して、全体攻撃。敵全体にダメージ、とかやらなきゃならない。逆に、もちろんオブジェクト自体も複雑化するということです。

staticなんかとても呼んでられないですよ。
これをstaticで書ききるのは、それなりの技術と記憶力がないと無理でしょう。ただしそれをstatic宣言群の記憶で使い切るのは非常に勿体ない。

オブジェクト指向は「忘れる」ためにあるのですから。

前出の例で言うと、BattlePersonをimplementsすると言うことは、「攻撃相手に指定できる」ということでもあります。処理方法は、PlayerPersonを実装する時に頑張ればいいわけです。メソッドの向こうは、一旦忘れるべき。それが「カプセル化」の本来の意図でしょう。privateだのprotectedだのgetter/setterだのは、ガイドライン或いはコーディングルールの一つでしかない。

故に「プログラムが追いにくくなる」は全くの言いがかりです。或いは、逆ギレです。
  • そういう脳の使い方に慣れてないだけ。もしくは、
  • そのプログラムが「オブジェクト希望プログラミング」で書いてあるか。
  • メソッド名のネーミングセンスが最悪か
ああ、ちなみに小生は関数型では作れません。知人に居ます。ほとんど仙人ですけどね。

え、「派生クラスの話は何処?」かって?それも説明しなくちゃ駄目?

例えば、attack()の中身の話をしていません。相手がモンスターだったりプレイヤーだったりで幾つか都合が有りますが、

  • 攻撃力を求める
  • 防御力を求める
  • ダメージ計算をする

という辺りは変わりません。
public function attack( BattlePerson & $target ){
$offence = $this->attackValue();
$defence = $target->defenceValue();
$damage = $offence - $defence;
$target->damage($damage);
}
abstract function attackValue();
abstract function detenceValue();
abstract function damage($damage);
そんな訳で、前にはinterfaceってことにしましたが、この関数は基底クラス預かりにしたほうが良さそうです。攻撃力、防御力は、プレイヤー、モンスターで別系統の処理が必要なので、派生クラスの責任にするしかありません。

後は実装の内容ですが、派生クラス間で、共有したい処理が幾つか出てくる筈なので、それを基底クラスに押し込むか、mix-inを使うか。もちろんPHPではmix-inは無理(ってことになってる)ので、入れ子クラスにするしかなかったりします。入れ子クラスの処理が追いにくいのは確か。Java族の悪しき伝統です。