オブジェクト指向設計ってなにがいいの?
プログラムを書くのにはオブジェクト指向がよいとされていて、Wikiには以下記載がある。
互いに密接な関連性を持つデータとメソッドをひとつにまとめてオブジェクトとし、それぞれ異なる性質と役割を持たせたオブジェクトの様々な定義と、それらオブジェクトを相互に作用させる様々なプロセスの設定を通して、プログラム全体を構築するソフトウェア開発手法である。
…これを読んで「オブジェクト指向すげーな、俺もオブジェクト指向で設計するわ」と思えなかったので、なぜオブジェクト指向で設計するのかを書いてみることにした。
※基本は知ってるという方は、ゲームで身につけるオブジェクト指向設計(実装編)をどうぞ。
実は人間の思考に近く直感的で簡単な考え方
プログラムは、変数に値を詰めるとか、if文で分岐したいとか、for文で繰り返したいとか、ちょっとまとまった処理は関数にするとかを、組み合わせて目的の処理を行うということをまずは習うと思う。
ただ、この基本的な考え方だけではフォートナイトやAPEX1みたいなゲームを作ろうとすると相当難しい。
フォートナイトのゲーム画面には、プレイヤーキャラクターや、地面などの背景、様々な武器や弾といったオブジェクトが存在し、それぞれのオブジェクトがプレイヤーの入力や、AI、物理法則などに従い独立して動作している。
そのようなオブジェクトの集合を処理するプログラムを書くのであれば、if文やfor文や関数の集合ではなく、オブジェクトの集合を操作するプログラムとして記述する方が人間の思考に近く直感的で簡単であるのは想像に難しくないと思う。
以下「プレイヤーが弾を撃つシーン」のシーケンスを示す。
直感的に設計ができるかを考えてみて頂きたい。
この図から、細かいプログラムは分からなくても、何を作ろうとしているかが想像できたとすればオブジェクト指向がなぜ必要なのか、いかに人間の思考に沿った設計手法なのかの半分ぐらいは理解できたのではないかと思う。
ここからは「プレイヤーが弾を撃つシーン」を例に、オブジェクト指向の以下概念について確認する。
- クラスとインスタンス
- クラスの属性と操作
- 派生
クラスとインスタンス
先のシーケンス図で武器から弾を発射する処理の、<<create>>
では「クラス」から「インスタンス」を生成している。
ゲーム中の弾は「武器より発射されて、何かにぶつかるかそれぞれの武器に設定された飛距離を飛ぶまではゲーム世界中に存在するもの」として定義されている。
その弾の定義が 「クラス」 であり、ゲーム中に実際に存在している弾のオブジェクト が 「インスタンス」 である。
以下に関係性を示す。
クラスの属性と操作
クラスには「属性」と「操作」がある。
具体例があった方がイメージしやすいと思われるので、先のクラス図に「属性」と「操作」を書いておいた。
属性(attribute, property)
「属性」は、弾の種別(マシンガンの弾なのか、スナイパーライフルの弾なのか)の様に生成された時点で決定され変化しない値と、速度や座標などの様に変化する値がある。
属性については、インスタンスの外側からの操作可否を設定できる言語が多い。
ここの設計は相当センシティブで設計に個人差が出たり、設計の難易度を上げるところと感じているが、ざっくり理解してほしいのは**「実装した自分以外が操作できてよい値かどうか?」**ということである。
例えば、弾の種別をインスタンスの外から変更できるように設計するということは**「弾クラスの設計・実装者が意図しないところで、マシンガンの弾がスナイパーライフルの弾に変化するバグが発生する可能性を含む設計」** とするということである。
このバグによってビクロイ2を取りそこねたユーザーなどがいた場合のことを考えてみよう。
サポートに熱量の高いメールが届き、即時解決をしなければならないだろう。
ところが、インスタンスの外から変更できるということはコード中のどこででも変更できる設計であるため、問題箇所の特定が非常に難しく即時解決は困難。
最悪、弾クラスの実装者だけではなく、実装者全員を呼び出して原因の特定を行う必要が出たり、返答が遅滞しSNSでサポートへの避難を拡散される。
想像しただけで恐ろしい。
操作(method)
弾が自立して動作する前提で話を進めていたが、自立して動作するためには「操作」が必要である。外部から「操作」を呼び出す事で適切に動く様な method 設計を行う必要がある。
例えば、1/60秒毎3に外部から1/60秒時間経過したから、『その分動いて』と「操作」を呼び出しその通り弾が動く設計にすれば、座標(x,y)などは外部に公開する必要がなくなる。
このように不用意に「属性」を公開せず、適切に「操作」を設計することを「カプセル化」と言うが、「カプセル化」に執着しすぎるとかえって設計が複雑になったりする事もあるので注意が必要である。
派生(inheritance, super class, delivered class)
マシンガンの弾もスナイパーライフルの弾も、どっちも実弾なので先のBulletクラスで実装できていた。
3年も運営してたらマンネリ化してきて、レーザー兵器やホーミング武器も欲しくなったり、有名タイトルとコラボしたくなったりするかもしれない。
その場合、シンプルなBulletクラスのままでは対応できなくなる。ホーミングさせるには記憶させておかないといけないパラメータが大量にあるため属性に保持しなければ行けない項目が増えるし、ホーミングさせるための「操作」も記述しないといけない。
Bullet クラスに、それらのコードを全て書いてしまうと超巨大なクラスとなりメンテナンスが困難になる。そこで、どんな弾にでも必要な属性と操作を基底クラス(super class)に定義し、レーザー兵器、ホーミング武器それぞれ固有のコードを派生クラス(delivered class)に定義する様な設計を行う事が考えられる。45
おわりに
入門書でフォローされない「なぜ」を中心に解説してみました。
「弾」というオブジェクトはインスタンスをイメージしやすい例かと思いますが、業務アプリでも考え方は同じです。
業務をよく観察して、エクスプローラのフォルダを整理するように、クラスを切り出してみてください。
設計レベルの「階層」を意識して、外部設計と1対1対応するビジネスロジック層のクラス、なんかよくわからない細かい処理を分かりやすく整理してまとめたライブラリ層のクラス、アプリ固有のコードなどに分けるとなんとなく整理されるかもしれません。