オブジェクト指向プログラミングを極める

より良いプログラマを目指すブログ

オブジェクト指向が劇的に上達するたった一つのルール「-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命名していたら、駒のスクリーン上の位置を誰が管理すべきかは明白だっただろう。
私はこれまであらゆるボードゲームのプログラミングをしているときに、このような「ゲーム自体の知識とアニメーションの知識が同居する」問題に悩まされていたが、名前の付け方を変えるだけであっという間に解決してしまったのだ。

そもそもオブジェクト指向的な発想ではない

よく考えてみれば、そもそも Getter/Setter を用意したデータオブジェクトを「なんとかer」に突っ込んで処理してもらい、結果を得るというのは構造化プログラミングの発想である。
一方、上記に挙げた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氏が提唱したルール。このスライドに詳しい。