Hatena::ブログ(Diary)

達人プログラマーを目指して このページをアンテナに追加 RSSフィード Twitter

2011-03-17

ドラゴンボールで学ぶオブジェクト指向 改

ドラゴンボールといえば、大変に人気の高い国民的、いや世界的な漫画、アニメですが、昨日匿名ダイアリーでドラゴンボールをネタにしたオブジェクト指向の解説がホッテントリに入っていました。

ドラゴンボールで学ぶオブジェクト指向

多くの人に親しみやすい題材でオブジェクト指向の考え方を解説するというのは非常に興味深い試みなのですが、オブジェクト指向の説明としては不適切なところがあり、ちょっと残念な内容になっています。私自身ドラゴンボールの専門家ドメインエキスパート)ではないため、不正確なところがあるかもしれませんが、ストーリーを思い出しながら、私なりにドラゴンボールをネタとしたオブジェクト指向の解説にリトライしてみたいと思います。

なお、オブジェクト指向でもプログラミング言語によって表現できる内容が異なるため、当然設計技法は違ってきます。ここではJavaC++C#Visual Basicといった静的な型付けの古典的なオブジェクト指向言語*1で設計することを念頭において説明することにします。

オブジェクト指向理解の第一歩はクラスとオブジェクトの正しい区別から

オブジェクト指向で設計するためにはまず、問題領域の概念を抽象的に表現するためのクラスを正しく抽出する必要があります。抽象化というと哲学的で非常に難しそうに聞こえるかもしれませんが、日ごろから我々は無意識にこの抽象化を行って物事を判断したり考えたりしています。たとえば、会社に通勤するのに電車を使っていることを想定してみてください。我々は「線路の上を電気の力で走る箱型の乗り物」を「電車」として認識しているわけです。実際に乗り込む車両は毎日違っているはずですが、とにかくそれを電車として認識できているのは細かい部分を無視して本質的なポイントを抽出する抽象化を無意識のうちに行っているからに他なりません。オブジェクト指向では抽象化によって名前を与えられた概念をクラスと呼んでいます。一方、毎日通勤時に乗車している一台一台の電車そのものはクラスのインスタンス(一例)であり、オブジェクトと呼ばれます。まずは前提知識としてこのことをしっかりと頭に入れてください。

電車に限らず、一般的に辞書に載っている名詞は抽象度の差こそあれ具体的なものに共通する概念に名前を与えたものです。ですからこれらはクラスの候補となり得ます。ただし、難しいのは名詞は共通概念としてのクラスを表すこともある一方で、具体的な一つ一つのオブジェクトを指し示す場合もあるということです。電車といった場合、概念的に電車というクラスを示す場合もあれば、今乗っている電車のオブジェクトそのものを指す場合もあります。

だから、時としてクラスとオブジェクトとの混同が起こります。多くのオブジェクト指向の解説書では問題領域の名詞を抜き出してクラスとして抽出すればよいと説明していることもありますが、実はそんなに単純ではなく、初心者の人はクラスとオブジェクトをきちんと区別して分析できないことがあります。たとえば、元ネタの記事ではクリリン、べジータなどのキャラクターをすべてクラスとしてモデル化しています。実現したいプログラムの機能によってはこのような設計を行う可能性もあるかもしれませんが、これは一般的には適切な設計ではありません。確かに、クリリンとヤムチャでは身長などの見かけや繰り出せる技も違いますから、一見別々のクラスのように思えてしまいますが、抽象的に考えてどちらも地球人の武道家であるとすれば、共通している事項も多くみられます。

よって、抽象化してしまえば、クリリンもヤムチャも共に「地球人武道家」というクラスに属するオブジェクトであると考えてしまうことができます。それでは、悟空やピッコロさんはどうでしょうか?これらのキャラクターは地球人ではありませんし、変身したり、傷を自己修復するなど地球人にはない特別な振る舞い(メソッド)を持っています。クリリンがどのようにがんばって修行してもそのような能力を習得することは不可能なのであり、クリリンたちと同一のクラスで表現することには無理があります。よって、「サイヤ人」「戦闘型ナメック星人」という別のクラスを抽出し、それらのオブジェクトであると考える必要があります。では、人造人間のセルはどうでしょうか?セルには他人を吸収することで戦闘力を向上させ、完全体に進化するといった特殊な能力があります。このような能力を持ったオブジェクトは劇中では他に存在しません。ですから、セルそのものはそういった特殊な生物を表すクラスなのですが、そのインスタンスであるオブジェクトは一体しか存在しないということになります。一般的にはクラスに対しては複数のオブジェクトインスタンスが存在するのですが、このようにインスタンスが一つしかない特殊なクラスもあります。*2

ここまで説明したことをもう一度整理すると

  • クラス*3→複数のオブジェクトが共通で満たす性質(属性や振る舞い)を概念として表現したもの。(地球人武道家、サイヤ人、戦闘型ナメック星人、種としてのセルなど)
  • オブジェクト→特定のクラスの性質を満たす具体的な人、物など(クリリン、ヤムチャ、悟空、ピッコロ、セルなど)

ということになります。そして、オブジェクトは必ず何かのクラスのインスタンス(例)となります。たとえば、クリリンは地球人武道家のインスタンスとなるオブジェクトですし、悟空はサイヤ人のインスタンスですね。*4

英語だと冠詞の有無でクラスとインスタンスの違いがより明確なところがある

ところで、残念ながら、日本語の文法には英語の「a」「the」などの冠詞や複数形といった概念がありません。そのため、英語で言うところのan appleもthe appleも冠詞のないappleもすべて「りんご」です。でも、英語ではこれらを厳密に区別しています。

Apple is one of the most popular fruits in the world. (りんごは世界でもっとも人気の高いフルーツだ)

この場合はりんごという概念について説明しているのでaやtheがつきません。*5この場合のりんごはクラスです。一方、

There is an apple on the table. (テーブルの上にりんごがある。)

の場合はりんごという概念ではなく、りんごのひとつのインスタンスが机の上に存在していることを表しています。ですからこの場合のりんごは一つのオブジェクトを表していることになります。*6このように、英語などを話す外国人の場合、可算名詞に対して無意識のうちにクラスとオブジェクトとを区別していることになるのですが、日本語だとその区別があいまいです。英語の冠詞については、以下の説明が参考になります。

冠詞

クラス間の継承関係(汎化・特化関係、is-a関係)を考える

オブジェクトとクラスを正しく区別することができるようになったら、世の中のさまざまなオブジェクトや概念に基づいてクラスを抽出することができます。このようにして抽出したクラスはまったく独立しているわけではなく、さまざまな関係で結び付けられています。

たとえば、先ほどの例では「サイヤ人」「戦闘型ナメック星人」「地球人武道家」「セル」のように別々のクラスを抽出しましたが、これらのクラスはお互いにまったく無関係というわけではありません。これらのクラスには

  • 戦闘力という属性を持っている
  • 複数の技を保持し、繰り出すことができる

などの共通の性質があります。このような場合、この共通の性質を満たす抽象的な概念に名前をつけて、別途クラスとして抽出することができます。この場合は「戦士」といったクラスを抽出することができるでしょう。このような上位の概念を親クラス(スーパークラス)と呼びます。つまり、「戦士」は「サイヤ人」や「地球人武道家」の親クラスです。逆に「地球人武道家」は「戦士」の子クラス(サブクラス)と呼ばれます。このような親子関係を汎化関係と呼びます。(厳密にはちょっとニュアンスが異なりますが)一般的なプログラミング言語ではこのような関係はクラスの継承によって表現するため、継承関係とも呼ばれます。文字通り親クラスの属性やメソッドを子クラスが継承することができます。*7こうした関係をUMLのクラス図を使って以下のように表現するとお互いのクラスの関係が理解しやすくなります。

f:id:ryoasai:20110317234442p:image

クラス図において、白抜きの三角矢印の線が継承関係を表しています。子供から親に向かって線を引きます。

なお、継承関係で重要なこととして、一般的に子供クラスのインスタンスは親クラスのインスタンスでもあるということを理解しておきましょう。つまり、悟空はサイヤ人クラスのインスタンスですが、親クラスの戦士のインスタンスでもあります。

クラス間の関連(has-a関係)を考える

次に、戦士が繰り出す技について考えてみます。技にはかめはめ波や太陽拳などさまざまな種類のものが登場します。これらは、それぞれ効果もまったく異なるため、別々の子クラスとして抽出するのが適切です。*8先ほど説明した継承関係が使えるということです。一方、戦士は一般的に複数の技を習得して保持しています。このようにあるクラスのインスタンスが別のクラスのインスタンスを保持しているような関係は実際よく登場します。このような関係は先ほど説明した継承関係とは異なり関連と呼ばれます。*9重要なポイントとして関連には多重度(カージナリティー)というものがあり、1対1、1対多、多対多などの種類に分けられます。戦士は複数の技を保持でき、また、技も複数の戦士によって保持されますから、戦士と技の関係は多対多の関連になっています。この関係をクラス図で表現すると以下のようにモデル化できます。

f:id:ryoasai:20110319101316p:image

Commandパターンを使ってさまざまな技をポリモーフィックに表現する

戦士は修行したりして成長するにつれて新しい技を習得していきます。*10新しい技を習得すればそれを繰り出すことができます。従来のオブジェクト指向でないプログラミングであれば、新しい技(機能)が加わるにつれてswitch文のような分岐が増えていくことになります。呼び出し側で処理を呼び分ける必要があるからです。しかし、オブジェクト指向言語ではどんなに技の種類が増えても、常に「繰り出す」という共通のメソッドの呼び出しで実行することができるのです。どうして、こういうことが可能かというと、戦士クラスが、かめはめ波クラスなど具象的な技のクラスと関連付けられておらず、抽象的な技と関連付けられているからですね。個々の技がどのような効果を持つかにかかわらずこれらは技という共通のクラス(Javaの場合、インターフェース)を継承しており、常に「繰り出す」という共通のメソッドの呼び出しで実行することができるのです。技ごとに別々のメソッドを呼び分ける必要がありません。このように共通のメソッド呼び出しで、対象とするオブジェクトの種類に応じてまったく異なるさまざまな処理を実行可能な性質をポリモーフィズム多態性)と呼びます。

最初はちょっと理解するのが難しいかもしれませんが、たとえとして、ここではドラゴンボールのRPG的な(アクションゲームだと操作が複雑なので)ゲームを想像してみましょう。キャラクターの戦士が新しい技を習得するにしたがって、戦闘時に選択可能な技は増えていきます。この場合、どの技を実行するにしてもゲームのユーザーがすることは適切な技を画面の一覧から選択してAボタンを押すことです。どの技を選択したかにより実際の攻撃の効果はまったく異なるものになるのですが、ユーザーの操作は常にAボタンの押下で一定しています。どの技を実行するかで操作方法をswitch文的に変える必要がありません。そんなことになっていたらユーザーは操作を覚えるのが大変でいやになってしまうでしょう。これはまさにポリモーフィズムの例になっています。

なお、初心者のうちは、ここで示したクラス図のような関係をいきなり思いをつくことが困難かもしれませんが、実はこのようにあるオブジェクトが複数のオブジェクトの集合を保持して、その機能をポリモーフィックに呼び出すというパターンは実際よくあることなのです。WordなどのGUIアプリケーションのメニューバーやツールバーRPGのコマンドメニューなどもそうですね。このような設計上の常套手段はデザインパターンとして知られていますが、特に今回のようなパターンはCommandパターンと呼ばれます。こうしたデザインパターンを知っていれば、ポリモーフィズムを活用した適切なクラス設計を思いつくのが簡単になります。

サイヤ人の変身をStateパターンで実現する

次に、サイヤ人の変身について考えてみます。サイヤ人は他のクラスの戦士と違って、大猿に化けたり、スーパーサイヤ人になったり、まったく別の戦闘能力を持つ状態に動的に変化することが可能です。Javaのように実行時の動的なクラスの変更が認められない言語では、こうした状態を表現するにはちょっとした工夫が必要となります。悟空などのオブジェクトのクラスを途中で別のクラスに変更することはできないのです。そこで、以下のように設計してみることにしてみます。

f:id:ryoasai:20110318020041p:image

悟空というキャラクター(魂?)は変えられないのですが、特定の条件を満たすことで大猿やスーパーサイヤ人などまったく別の肉体に変化すると考えるのです。この場合、我々が衣服を着替えるのと同じ発想で、サイヤ人が複数の肉体と関連を持つことができると考えます。同時に使用可能な肉体は一つという制約がありますが、状態によってどれかの肉体を選択して闘うと考えればよいのです。このように状態によって関連付けられるクラスを切り替える手法はStateパターンとして知られています。

フュージョンCompositeパターンで表現できる

最後に、ちょっと難しい例としてフュージョンについて考えてみましょう。劇中ではトランクスと悟天が合体してゴテンクスという強力な戦士が誕生する話が魔人ブウの話で出てきます。厳密にはフュージョンはサイヤ人固有の技ではなかったと思いますが、劇中ではサイヤ人のみが使っていたため、ここではサイヤ人固有の技として考えることにします。

どのような設計が考えられるでしょうか。ここで、ゴテンクスというクラスを作るということを考えてしまった人は残念ながらまだまだクラスとオブジェクトの違いがよく理解できていない人です。ゴテンクスについて、もう少し慎重に分析してみると

  • 2人のサイヤ人が合体することで誕生する
  • 合体後も、もともとのキャラクターの性質を保ちつつもまったくの能力の異なる戦士が誕生する

などの性質があることがわかります。したがって、以下のクラス図のように「複合サイヤ人」というサイヤ人のサブクラスを作成することにしましょう。

f:id:ryoasai:20110318014309p:image:w600

そして複合サイヤ人にはもともとの合体前の2人のサイヤ人と関連付けされています。このように複数のオブジェクトを組み合わせて、一つのオブジェクトのように処理するパターンもよく登場するのでCompositeパターンとしてパターン化されています。このパターンは他の戦士を吸収するセルやブウの設計にも応用できると思います。

まとめ

ここではドラゴンボールの戦士をモデル化することを例としてオブジェクト指向設計の基本的な考え方について説明しました。

  • クラスとオブジェクトとの違い。クラスは共通の概念、オブジェクトはどれかのクラスに属する一つ一つのものである。
  • クラスは概念を抽象化したものなので、戦士や複合サイヤ人などのように登場する具体的なものから直接には連想しにくいものもある。(適切なクラスを発見できてこそ一人前のオブジェクト指向プログラマーといえる)
  • さまざまなクラスは共通の性質に着目することで継承関係をモデル化することができる。
  • クラスは戦士が技を持つというような関連を持つことができる。
  • クラスが共通のクラス(インターフェース)を実装することでポリモーフィズムを実現することができる。
  • クラスの設計を行ううえではデザインパターンなどのパターンが役に立つ。

なお、ここではドラゴンボールを例としてオブジェクト指向の考え方の基本を説明しましたが、実際にはオブジェクト指向プログラミングを行うことではじめて深く理解することができるものだと思います。私のブログではJava認定試験のリファクタリングを題材にした以下も参考にしていただけると思います。

Javaプログラミング能力認定試験課題プログラムのリファクタリングレポート(その1) - 達人プログラマーを目指して

Javaプログラミング能力認定試験課題プログラムのリファクタリングレポート(その2) - 達人プログラマーを目指して

Javaプログラミング能力認定試験課題プログラムのリファクタリングレポート(その3) - 達人プログラマーを目指して

続編も書きました。

ドラゴンボールで学ぶオブジェクト指向 改(第弐話) - 達人プログラマーを目指して

*1C++の設計者であるストラウストラップの定義に近い実装になっている言語

*2:広い意味でシングルトンと呼ばれることもあります。

*3:クラスという言葉には本来階級分けとか分類という意味があります。学校のクラスなども生徒を分類したものです。本来の用語の意味としては、クラスはオブジェクトを種類ごとに分類する手段と考えることもできます。ただし、分類の手段として概念の説明が付随するため、「クラスは概念」という説明をされることが多いと思われます。

*4:ここではオブジェクトインスタンスは同義語として使っています。厳密にはオブジェクトは「もの」そのものを指し、インスタンスといった場合、どのクラスに分類されるオブジェクトかという点を強調する意味があります。

*5:英文法の教科書に出てくるA dog is a faithful animal. という文は犬という一般概念について説明しているのにaが付いています。この場合、犬が忠実な動物であることを典型的なインスタンスを頭に思い浮かべることで描写しているためと考えられます。つまり、クラスではなく、JavaScriptのようにプロトタイプインスタンスを元に一般の性質を考えていることになります。

*6:実際、Smalltalkなどのコーディング規約ではオブジェクト名にaやtheの接頭辞付けるケースもあります。

*7:厳密にはフィールドやメソッドの実装を親クラスから継承することに加えて、型が継承されるということを区別する必要がありますがここでは説明しません。

*8:実際には、開発したいプログラムユースケースに依存します。単に技のリストをデータとして保持するのが目的なのであれば、サブクラスを作成せず技自体を具象クラスにしてデータを保持させれば十分です。ここでは、ゲームなどのように個々の技のロジックにバリエーションあるようなケースを想定しています。

*9:場合よって集約、コンポジションとも呼ばれる関係となります。ここでは詳細の説明は割愛します。

*10:この動的な性質により、通常のオブジェクト指向言語ではある時点で戦士が保持する技の種類ごとに個別に戦士のクラスを作ることは不適切です。

rising3rising3 2011/03/18 12:37 整理されていて面白いです。
ただクラス図でクラス固有の状態がメソッドに現れている点が少し違和感があったのでコメントをします。
※説明しやすくしただけだと思いますが・・・

【セルクラス】
「吸収する()」はメソッドでもいいかと思います(ただ、一般人も吸収しているからインタフェースは考慮が必要かも)が、「完全体に進化する()」は、最終的に吸収したエネルギー(戦闘力(?))で、状態が変化するのでメソッド一発変身というのは違和感がありました。
(たしか、何段階かの変身もあったし、ごはんにゴボゴボにやられた時に完全体の状態もとけたような気がするので。フュージョンの亜種的なものでも表現できるかな。)


【サイヤ人クラス】
大ザル化は、1700万ゼノ以上のブルーツ波によって大ザル化するという定義みたいなので、メソッドより体に受けたブルーツ波の量で大ザル化するが面白いかな。
スーパーサイヤ人化も怒り量みたいな概念があって、いわゆる「ぷっつん」でスーパーサイヤ人化しているみたいです。最初は仲間がやられたりすることで「ぷっつん」状態になって、スーパーサイヤ人化してましたが、慣れてくると「ぷっつん」した時の状況を思い出し、意図的に「ぷっつん」状態になってスーパーサイヤ人化してるだけかな。

すみません、細かいコメントで^^;

ryoasairyoasai 2011/03/18 12:50 詳細なコメントをありがとうございます。

完全体になる()とかスーパーサイヤ人になる()というメソッドはFacadeパターンのようなものをイメージしていましたね。これらのメソッドが外部から呼び出された場合、実際に中にあるStateなどのオブジェクトが作り変えられるような感じです。
つまり、戦士のサブクラスは本質的に非常に複雑なロジックを内包しており、多数の粒度の細かいオブジェクトに対するAggregateやServiceのような役割もあるということで。

もちろん、最終的にはこれらのクラスで何を実現したいのかというユースケースに設計は依存しますね。キャラクターの振る舞いを完全にシミュレートするのが目的であれば、もっと違ったモデルになると思います。

asdasd 2011/03/18 20:02 セルがシングルトンというのはちょっと違和感があります。
作中に一体しか存在していないとはいえ、一体しか存在してはならない(存在しえない)わけでは無いですし、ほぼ永続的に存在するものでもありません(セルは悟飯に倒されGCされています)。セルのクローンのような敵(他のインスタンス)が現れるシーンを想像するのは容易です。
インスタンスが一つしかないからシングルトンというのは、シングルトンの濫用のように思います。

ryoasairyoasai 2011/03/18 20:41 少なくともGoFのSingletonパターンではないですね。このパターンではインスタンスが一つであることを保障するというのが目的の一つですので。ですので、あえてSingletonパターンとは説明しませんでした。ただ、特異な振る舞いを持つインスタンスのことをシングルトン(特異オブジェクト)と呼ぶこともあります。実際、RubyやScalaでは特定のオブジェクトに限定してメソッドをmixinするようなことが可能です。よって、広い意味でこの用語を注釈でこっそりと出したのですが、混乱するため不適切だったかもしれません。
あと、概念モデルとしてはセルがシングルトンというのはやや不自然かもしれませんが、実装の最適化のため実装モデルとしてはシングルトンを使って実装することはあり得ると思います。