オブジェクト指向が劇的に上達するたった一つのルール「-erで終わるクラスを作るな」
私が出会った、オブジェクト指向エクササイズ*1を凌ぐほどの素晴らしいルール。
「-erで終わるクラスを作るな」
私はこれを見たとき何言ってんだこいつと思った。私が当たり前のように作ってきた Renderer、Manager、Helper、Controller…などのクラスは間違いだと言うのだ。むしろこれらのクラスを「作り出す」ことができるということがオブジェクト指向をマスターしている証だとさえ思っていた。
しかし、このルールを実践した後、私の考えは180度変わった。
Renderer、Manager、Helper、Controller…
これらのオブジェクトは現実に存在しないクソオブジェクトだ。
現実に存在しない架空のオブジェクトを作ってはいけないのだ。
今すぐ「なんとかer」をやめるべき理由
責務の配置を間違う
「なんとかer」というオブジェクトは現実に存在するオブジェクトではなくプログラマーの都合により生み出された架空のものだ。なので本来負うべきではない責務を負わせたり、逆に負うべき責務に気付かなかったりする。
例として、私が作っていた将棋ゲームの汚いコードの一部を示す。
class Koma { int kind; // 駒の種類 int boardX, boardY; // 盤上の位置 int timer; // アニメーションタイマー float posX, posY; // スクリーン上の位置 int status; // アニメーション状態 // 盤上の位置(destX, destY)に移動できるかどうか返すメソッド public bool CanMove(int destX, int destY) { // 駒の種類によって動けるかどうかを判定 } // 駒を移動する public void Move(int destX, int destY) { // 盤上の位置を移動 boardX = destX; boardY = destY; timer = 30; //30フレームかけて移動 status = 1; //アニメーション状態を「移動中」にセット } // 駒の更新(画面更新毎に呼ばれる) public void Update() { if (status == 1 && timer > 0) { posX += (CELL_SIZE * boardX - posX) / 10.0f; // 目標へ滑らかに移動する posY += (CELL_SIZE * boardY - posY) / 10.0f; timer--; } // ...以後アニメーション関係の処理(500行くらい) } // 駒をスクリーンに描画 public void Render(IKomaRenderer renderer) { renderer.Render(this.kind, this.posX, this.posY); // Rendererに処理を委譲 } } // 駒の描画クラス class KomaRenderer : IKomaRenderer { public void Render(int kind, float posX, float posY) { Graphic g = GraphicManager.Get(kind); g.Draw(posX, posY); } }
駒クラスは駒の種類、盤上の位置、スクリーン上の位置を管理していて、盤上の位置が変更されると内部でアニメーションのステータスが変更される。
そして60分の1秒ごとにUpdate()
が呼び出されることで滑らかに移動する。
駒の描画はKomaRenderer
に委譲されておりインターフェースを介して駒をスクリーン上へ描画する。
問題点はお分かりだろう。描画こそ別クラスに委譲されているが、Koma
クラスには依然として将棋に関する知識とアニメーションに関する知識が同居している。そのためKoma
クラスの役割はその名前から想像できる範疇を超えており、コードは1000行近くまで醜く太っていた。
なぜこうなったのか。このコードを書くときに私はこう考えていた。
駒のスクリーン上の位置は誰が管理するのが適切か?駒に関係することだからやはり
Koma
か?Renderer
は描画することに集中するべきだし「描画する人」が座標をプロパティとして持つのはどう考えてもおかしい…。だからRenderer
ではないだろう。ならばやはりKoma
しかない。
こうして無事に1000行近くのKoma
クラスが誕生したのである。
もしこの時、KomaRenderer
ではなくRenderedKoma
と命名していたら、駒のスクリーン上の位置を誰が管理すべきかは明白だっただろう。
私はこれまであらゆるボードゲームのプログラミングをしているときに、このような「ゲーム自体の知識とアニメーションの知識が同居する」問題に悩まされていたが、名前の付け方を変えるだけであっという間に解決してしまったのだ。
今あるクソオブジェクトをどうやって置き換えたらいいのか
ではどうしたらいいのか。ルールを実践した結果いくつかヒントを得られたので下記に紹介する。コツとしてはそのオブジェクトが本当に表しているものを考えると良い。
Manager
みんなこの名前が大好きだ。このクラスは色々な責務を担当させてManagerという名前を付けてごまかしているのてだいたい神クラスと化す傾向がある。
もしこのクラスが単にオブジェクトのコレクションならそのままCollection、List、またはその複数形(例:CardManager → Cards)を名付ける。もしこのクラスが何かリソースの管理をしているなら、それが管理しているモノの名前を付ける(例:×MemoryManager → ○Memory)。
もしこのクラスが複数のオブジェクトを連携させるための仲介役(GameManager等)であるならこれを廃してオブジェクト同士が直接連携するようにする。そうすると結合度が高くなりすぎるというときは設計がそもそも間違っているのでオブジェクトを小さく分割して責務を再配置したりインターフェースを介して連携したりすることを検討する。
Controller
Managerの仲介役の場合に同じ。
Renderer, Printer
これらはオブジェクトの中身を外界に表現するときに使われる。Rendererという名前は表現する手続きに着目しているのでよろしくない(→手続き型への退行)。代わりに表現されたオブジェクトの方を命名する。PlayerRenderer
ではなくRenderedPlayer
という風に。
Helper
これらは大抵の場合、データベースとかネットワーク通信とかごちゃごちゃした手続きを押し込めるために作られる。
カプセル化の真意を思い出し、手続きの結果得られるモノの方をオブジェクトとして名付ける。
HTTPHelper
ではなくWebPage
という風に。
いかがだろうか。技術書はもちろんのこと.NETやAndroidなどの大御所にまで登場する「なんとかer」が間違いだというのはにわかに信じがたいかもしれない。しかし、何か複雑な処理をするものに無理やり"なんとかer"と名付けてクラスを「作り出す」考えから脱却することでオブジェクト指向の極みを垣間見ることができるので是非実践してみてほしい。
*1:ThoughtWorksアンソロジーの中でJeff Bay氏が提唱したルール。このスライドに詳しい。