一度理解してしまうと、もうそういう風にしか考えられなくなるけれど、理解するまではちょっと難しい、そんな「オブジェクト指向プログラミング」ですが、その理解に少しでも近づけるように、僕なりのポイントを4つ程挙げてみました。突っ込みどころも多いとは思いますが、僕なりの考えです。オブジェクト指向プログラミングはやってるけれど「何かあまりしっくりと来ていないな…」という方へ、ちょっとでも参考になれば幸いです。
1. クラスと継承は、忘れる
オブジェクト指向プログラミングの解説で、まず最初に「クラス」と「継承」の説明がなされることがよくあります。間違いでは無いでしょうし、多くのオブジェクト指向プログラミング言語ではクラスはとても重要な役割を担います。Java はクラスが無ければオブジェクトを作ることも出来ません。しかし、やっぱり一旦忘れてください。クラスや継承は、オブジェクト指向プログラミングを理解するにうえで、大きな誤解を生むことがあるからです。
「継承によって、あるオブジェクトが他のオブジェクトの特性を引き継ぐ」—— 継承についての説明です。しかし、これを単に「機能を効率的に(あるいは簡単に)受け継ぐ手段」だと理解してしまうことがあるようです。でもそれは間違った理解です。ここでの間違った理解は、典型的な「ごった煮クラス」を作成してしまう罠の入り口です。便利だから、便利だからと、あるクラスに山のようなメソッドが実装されていきます。そしてやがて、アプリケーションの大半のロジックが1つのクラスに実装され、まるでベタ書きの PHP のようになったコードを見た事があります。
クラスと継承は、オブジェクト指向プログラミングを理解する上では雑音になります。一旦忘れてください。
2. 包含関係は重要
継承関係と同様に大切な概念として紹介される概念に「包含関係」があります。オブジェクト指向プログラミングの解説ではよく、継承関係を「is-a関係」、包含関係を「has-a」関係と説明されます。継承は忘れてくださいと書きましたが、実はこの「包含関係」こそ、オブジェクト指向プログラムの実現する上でとても重要な概念ですので、しっかりと抑えることが大切です。
「オブジェクトが別のオブジェクトを所有し、その機能を利用する」—— これが「包含関係」です。具体例は次のパートで挙げたいと思いますが、例えば、あるオブジェクトAが幾つかの機能を持つ時、その機能の一部を別のオブジェクトBに担当させ、オブジェクトAはオブジェクトBを所有することで、オブジェクトBの機能を利用することを言います。
極端な話、継承は無くてもオブジェクト指向プログラミングは実現できますが、包含の概念はとても重要です。どういうわけか、オブジェクト指向プログラミングの多くの解説において、継承についての説明はたっぷりあるものの、包含についての説明は小さな扱いになっているような気がします。そのような中でクラスと継承を学ぶと、この包含関係の重要性についての気付きが遅れてしまいがちになります。ですので、しつこいですが、継承は一旦置いておいてください。そして包含関係について、重要だと思って注目してください。
3. オブジェクトは役割を持つ
オブジェクトはそれぞれ、何らかの役割を担います。役割は明確であることが大切です。オブジェクトはそれぞれの役割を与えられて、その与えられたその役割について責任を持つ。これこそがオブジェクト指向プログラミングの本質です。
では、オブジェクトの役割分担とはどういうことでしょうか? 一例をあげてみます。テトリスのようなゲームのオブジェクト構成を考えてみましょう。ざっと Game(ゲーム全体)、Stage(ゲーム画面)、Block(落ちてくるブロック)が思いつきます。底に積み重なったブロックはどう表現しましょうか? Stage オブジェクトに、配列で持ちますか? プレイの制限時間はどうしましょう? Stage オブジェクトに終了時間を持って監視しますか? どうやらこの調子では、Stage オブジェクトはゲームのほとんどを管理する大きなクラスになってしまいそうです。
そこで、実際のスポーツの試合を考えてみましょう。コートの上にボールは有りますが、「コート」はストップウォッチを持って試合終了の笛を吹いたりはしません。それは審判やタイムキーパーの役目ですね。それと同様に、Stage は「コート」としての役目に徹してもらって、Timekeeper という別のオブジェクトを用意して時間を管理させ、StackedBlocks というまた別のオブジェクトを用意してステージ上に積み重なったブロックを管理させてはどうでしょうか。つまり、役割を分担するわけです。
|
var Stage = function() { this.timekeeper = new Timekeeper(); this.stackedBlocks = new StackedBlocks(); } var stage = new Stage(); |
これで少し役割が分担できました。Timekeeper オブジェクトは時間を管理する役割、StackedBlocksは積み重なったブロックを管理する役割を担うことになります。
また、上のコードで、Stage オブジェクトが Timekeeper オブジェクトと、StackedBlocks オブジェクトを持つことになりますが、これが「包含関係」です。Stage オブジェクトに Timekeeper と StackedBlocks が含まれて(包含されて)いますね。このように、「包含関係」は役割毎に存在する複数のオブジェクトを関連づけるための大切な方法の一つなのです。
オブジェクトを役割分担するメリットは、そのまま「オブジェクト指向プログラミングのメリット」と言えます。オブジェクトが役割毎に分けられると、それぞれのオブジェクトは小さく、そして単純になります。役割の少ない、小さなオブジェクトは、簡潔で見通しが良くなります。シンプルなコードは変更も簡単で保守もしやすいです。また、単純なロジックだけが含まれることで、バグが混入しにくくなります。プログラマは小さくて閉じた世界だけで考えることができるので、自信をもって「論理的に正しい」と言えるコードを書きやすくなり、不安も減るでしょう。また、十分に設計が上手いと、再利用性が高いオブジェクトを作ることもできます。このように、オブジェクトに明確な役割を持たせることで、様々な恩恵を得ることが出来るようになります。これが、オブジェクト指向プログラミングの最も重要な概念です。
4. オブジェクトは役割について責任を持つ
さて、役割が分担された小さなオブジェクトは、もちろん単体では動作しません。アプリケーションの中のオブジェクトは、互いに会話してメッセージをやり取りし、アプリケーションの動作が成り立っていきます。オブジェクト同士の会話は、一般にはメソッドのコールを通じて行われます。
ここで「会話する」という点の理解は重要です。いろんなオブジェクトが存在し、それらが相互に会話してアプリケーションが成り立って行く、という感覚です。そのように組み立てられたプログラムは、一連の処理があちらこちらのオブジェクトに散在することになり、プログラムが上から下に流れて行くようなイメージにはなりません。友人のPHPのプログラマーは、それを「非常に見難い」と評価していましたが、そういうものだと思う方が近道です。実装があちらこちらに散在することに違和感を覚える人は、慣れが必要です。
コツのひとつとして、プログラムの流れを追うのでは無く、オブジェクトがそれぞれの役割を全うすることを意識して見ることがとても大切です。もう少し分かり易く言うと、それぞれのオブジェクトや、オブジェクトのメソッドが正しく動くように小さな単位で考える、ということです。全体を見るのでも、流れを追うのでもありません。そのオブジェクトや、オブジェクトのメソッドが、それぞれ正しく動くようにだけ考えていくことが、役割分担されたオブジェクトと上手く付き合っていく為のコツです。
まとめと発展
以上について、まとめてみました。
- クラスや継承は、いったん忘れた方が、オブジェクト指向を理解し易い
- 「包含関係」は大切なので必ず抑える
- オブジェクトはそれぞれに個別の役割を持つものと意識する
- オブジェクトが相互に連携してアプリケーションが成り立つものと理解する
- プログラムは流れでなく、オブジェクトの単位で眺める
ちょっと極端な内容になってしまいましたが、オブジェクト指向がなかなかしっくり来ない方は、今一度リセットしてこの観点で学んでみるのも良いかと思います。学習にあたっては、言語仕様がシンプルでクラス等の雑音の少ない JavaScript が良いように思います。
もちろんクラスや継承も大事だけど…
「クラスや継承は一旦忘れてください」と書きましたが、やっぱり大切ですw。避けて通れない道でもあります。しかし、クラスや継承は、オブジェクト指向プログラミングを扱い易くするための一つの手法に過ぎず、オブジェクト指向の本質では無いことを忘れないでください。例えば「クラス」は、実装の継承手段やオブジェクトの雛形として非常に便利ですが、JavaScript のようにクラスという概念が無い言語でも、別の方法でその目的を実現するものもあります。クラスや継承は、どちらかというと、プログラムの実装の再利用性を高めるための一つのテクニックだと僕は認識しています。
オブジェクトの役割を常に意識する
クドくなりますが、オブジェクト指向で設計やプログラミングをする時は、個々のオブジェクトの役割をしっかりと考えて、それに必要なメソッド(機能)やフィールド(データ)を追加していくことが大切です。便利だからと、安易にオブジェクトに機能を追加するのではなく、「このオブジェクトは本当にこの機能を持つべきなのか?」と疑うぐらいで丁度良いです。場合によっては、新しい種類のオブジェクトが必要になります。慣れないうちは、なるべく擬人化して考えると良いかも知れません。「○○さんは、■■■が役割だから、▲▲▲▲▲は○○さんの仕事だよね」という感じです。
オブジェクトの粒度
オブジェクト指向で設計やプログラムを行っていると、時にオブジェクトが細かくなり過ぎることや、沢山のクラスやファイルが出来てしまうことに戸惑いを感じる事があるかも知れません。少し高度になると、沢山の種類のオブジェクトの存在がメモリや実行速度の問題につながることもあります。あるいは、単に技術的な扱い易さの問題で、1つのオブジェクトに2つ以上の役割を与えたくなることもあります。学習の時点では「役割は細分化する」で良いと思いますが、こと「実践」において、これについて明確に「どうすべき」という指針はありません。それはケースバイケースであり、様々な要素を含めて検討すべき問題です。つまり、実にそれこそが、オブジェクト指向の「設計」の重要なポイントなのです。ですので、個々のプロジェクトの中で、そこは十分に悩んでいただければと思います。
デザインパターン
オブジェクト指向でのプログラムの設計には、幾つかの便利なパターンがあります。最初の頃はお薦めしませんが、オブジェクトを中心に考えることが出来る癖が着いて来た頃に、是非それらを一度勉強してみることをお薦めします。「プログラミング」というものが、優れたアイデアによってより機能的に、よりシンプルに、そしてより美しくなる姿を見る事ができると思います。僕は昔 Java の Collection フレームワークを勉強してとても感動した覚えがあります。またオブジェクト指向による実装の抽象化という概念も、そこから学びました。そんな美しいプログラミングを目にして、感動して、よりプログラミングが好きになれたら僕はとても素敵だなーと思います。
以下に、僕的に「これ知っておいた方がよいなー」と思えるパターンを少しだけ抜き出して紹介してみます。まずは、とりあえず知っておきたい、そして実は良く見掛ける(いつの間にかあなたも使っている筈の)パターンです。説明は何も書きませんので、グーグル先生に教えてもらってください。
- Factory Method パターン
- Singleton パターン
- Observer パターン
また次は、これを知っていると、設計の幅がぐんと広がるゾー的なパターンです。Visitor パターンは僕も最近になって知りましたが、知らなかったのを不幸と思えるほどの強力な武器になりました。オブジェクト指向は奥が深いですね。
- Strategy パターン
- Adapter パターン
- Visitor パターン
以上です。ではでは、ハッピーコーディング!