はじめに
こんにちは。プログラマーのTです。
株式会社ジーンでは、社内でいろいろな勉強会が行われています。これまで2回にわたって、コーディング・テスト勉強会の紹介をおこない、プログラミングにおける、コードの見やすさについて取り上げてきました。
プログラムの構造
ここまでは人間が見やすいかどうか、理解しやすいかどうかを解説してきましたが、プログラムの構造については触れてきませんでした。今回はプログラムの構造について考えていきましょう。
一度に一つのことをする
プログラムの集まりであるクラスやモジュールにおいてよく言われることは、一つのクラス(モジュール)に一つの役割、というのがあります。
もちろん、クラス単位での役割も重要ですが、ここではコードブロックや関数などの小さい単位でも、同じように「一度に一つのことをする」ほうが、多くの場合で良いという話をしてみたいと思います。
HPバーの表示状態を変化させるプログラム
以下のプログラムを見てください。
ゲーム画面で、ライフなど、各種プレイヤーの情報を表示するPlayerUiクラスの例です。このプログラムでは、更新時に、プレイヤーの残りライフ値に応じて、lifeGauge_の状態を変化させています。例えば、ゲージの色を変えたり、明滅したりするといった処理を行っています。
もちろん、このプログラムで動作には何の問題もありません。しかし、このコードをよく見ると、判定を行うプログラムと、値を設定するコードがいくつか合わさってできていることがわかります。
つまり、二つのことを交互にやっているということになります。では、これを一度に一つのことをするように変更してみましょう。
まず、lifePointに応じて、LifeGauge::ViewTypeを計算します。そして、その結果を設定します。プログラムは次のようになります。
プログラムとしては行数が増えていますし、すこしわかりにくいようにも思えるかもしれません。こうすることでどのような具体的なメリットがあるでしょうか?
LifeGauge::setViewType()関数の呼び出しが3回から1回になっています。これによって、呼び出しの確認が容易になります。呼び出し回数が少ないと誤りが少なくなり、変更に強くなります。
getLifeGaugeViewType()は、lifePointとLifeGauge::ViewTypeの間の値の変換を行っているだけです。
LifeGauge::ViewTypeの数が増減したり、判定に使用する値が変化したり、条件が複雑になったときも、getLifeGaugeViewType ()関数のみを変更すればよくなります。
要求されるさまざまな変更に対して、変更の影響範囲が関数一つだけになっていることがわかります。
プログラムに問題があったとき、PlayerUiクラス全体をテストする代わりに、getLifeGaugeViewType ()の動作と、LifeGauge::setViewType()のそれぞれ個別にテストを行うことで、バグの検出が簡単にできます。この例ではあまり差がありませんが、規模や複雑さが大きくなると、個別にテストできるという利点は大きなものになります。
選択されたElementに処理を行うプログラム
別の例をみてみましょう。以下のコードは、画面に要素を配置し、レイアウトするエディタのコードです。エディタ内のelementが選択されていれば、その位置をoffset分移動するという簡単なプログラムです。
もちろん、この例で問題はまったくありません。
しかし、次のように書き直すことができます。
さて、プログラムとしては増えてしまっていますが、こちらでもどのようなメリットがあるか考えてみましょう。
選択された要素を取得する処理が、関数になり独立しました。選択された要素に対する処理を行うとき、この関数が汎用的に使えるようになりました。それによって、選択されているかどうかの判定がisSelected()というbool値を返す関数からenum値に変更されても、この関数の判定を1か所修正するだけで対応することができます。
また、移動処理をmoveElements()に分離しました。処理対象を呼び出し側からもらうことにより、選択状態以外にも、特定の種類のものや、すべての要素など、入力を変えるだけで動作を変更できるようになっています。
これは、「一度に一つのことをする」原則から、「要素を収集する」と「要素を移動する」という二つの処理に分けたことによる効果です。例えば以下のプログラムのように、拡張を行うことが容易です。このようにプログラムを書いていくと、規模の大きさに従って増えるコードの量を大幅に抑制する効果があります。
メリットとデメリット
ここまで、「一度に一つのことをする」プログラムについて解説してきました。もちろん、このようなコードの書き方が、すべてに優れるというわけではありません。具体的にどのような違いがあるでしょうか。ここまでの例の修正前を前者、修正したものを後者と呼びます。
まず、プログラムの実行速度は前者のほうが速くなる傾向があります。後者では関数呼び出しや引数のやり取りをする必要があるなど、速度面ではやや不利です。
後者のプログラムは、前者に比べると、関数にまとまっていることが多く、抽象的なわかりやすさに優れます。拡張性や変更への強さも優れています。しかし、役割がクラスごと、関数ごとに細かく分かれているため、実際にどのような処理になっているかを追いかけるのは前者に比べると少し難しい場合があります。
実際の開発では
前者のプログラムは、コードが大きくなると把握が難しくなる、重複するコードを生みやすいといった問題点があります。書き始めの時点では良くても、次第に見通しが悪くなり、変更や修正のコストが高くなりやすい傾向にあります。これは、実際に仕事で開発するようなプログラムではかなり致命的です。
大規模なプログラムや拡張性を考慮したプログラムでは、後者のように、機能を分割し、それを組み合わせるように書いたほうがほとんどの場合で良い結果になります。
まとめ
プログラムを書くときに、これは一つの処理のまとまりになっているか、複数の処理に分割できないか、と考えてプログラムを書くことで、より柔軟性や拡張性の高いプログラムを書くことができるようになります。
今回お話ししたのはコードの内容についてですが、もちろん、クラスやモジュール単位でも同じことが言えます。機能のまとまりとして、一つのことに注力しているかどうかは、健全なクラス設計においてとても重要なことです。
おわりに
今回は、コードの構造についてお話ししました。
昨今のゲーム開発では、開発規模が大きくなり、開発期間も長くなっています。
また、ソーシャルゲームに限らず、継続的にアップデートを行ったり、プラットフォーム移植を行ったりなど、ソースコードが利用される期間が、以前に比べるとはるかに長くなっています。
そういった環境にも対応できるよう、ジーンではさまざまな社内勉強会を行っています。
今回の内容も、すこしでも参考になればと思います。
解説は以上です。ありがとうございました。