2011-12-22
Java 的オブジェクト指向を 90 分で理解する
1. 分からない。いくら説明を読んでも分からない。
● 1.1. 未だに分からない Java 的オブジェクト指向
今日び Java 的オブジェクト指向の説明なんて星の数ほどあるような気がしますが、それでもなお「これで分かった!」という説明に辿りつけない不運な人がいるようですね。まぁこんだけ色々な説明が溢れていたら逆にどれを読めば良いのかワケ分からなくなってしまうのかもしれません。じっくり読んでも理解できなかったのであれば、きっとその説明と読者の相性が悪かったんでしょう。… というわけで、僕も Java 的オブジェクト指向が全っっっっ然これっぽっちも分からないという人に向けて説明する記事を書こうと思います。そうでない人には無価値な記事ですのでブラウザの「戻る」をクリックしましょう。
● 1.2. 「オブジェクト指向」という名の南の島がある
オブジェクト指向にはいくつもの専門用語があって、学習するのが大変ですね。専門用語の説明に専門用語が使われてたりして、ワケ分からなくなる事もあるかと思います。専門用語の説明に専門用語が使われるというのはどういうことかというと、その界隈には色んな文脈があるということです。そしてそれぞれについて語るべきことがあり、それぞれがそれぞれの知識背景を抱えて語っているということです。その深さや広さに唖然としてしまうかもしれませんが、一部分でも分かることができれば、あとはちょっとずつ分かる領域を広げていくことができるはずです。Java 的オブジェクト指向が分からないというのは、どこから学べば良いのかが分からないだけだと思います。
まぁそんなわけで、もしかしたらこの記事を読んで分かるようになるかもしれませんし、やっぱり分からないかもしれません。が、本当に Java 的オブジェクト指向が全然分からなくて、分かりたいと思っていて、この記事を読んでいる人は、僕が分かってる範囲で噛み砕いて説明するので、これを読むために 90 分を投資してください。「もっと短くてわかりやすい説明ぐらいいくらでもあるしwww アホかwww」と思うならブラウザの「戻る」をクリックしましょう。この説明を最後まで読んでもやっぱり分からなかった場合も、他を当たりましょう。その際にはこの記事の説明はきれいさっぱり忘れて読むと、もしかしたら今度こそ分かるかもしれません。例えば 『いまさらながらだけど、オブジェクトとクラスの関係を究めてみようよ』 檜山正幸のキマイラ飼育記 とか 『Javaのオブジェクト指向入門』とかの説明は分かりやすいように思います。
2. Java 的オブジェクト指向を理解するためにどうしても理解してほしいこと。
● 2.1. Java で書いたプログラムは上から下へ実行される
Java では上から書いた順に実行されます。
・A を実行する。 ・B を実行する。 ・C を実行する。
という感じでプログラムを書いたら A を実行して、終わったら B を実行して、終わったら C を実行します。
● 2.2. Java では「上から下へ実行する」部品を組み合わせて書く
↓ Java では、「上から下へ実行する」を部品にすることができます。
部品「買い物に行ってくる」 { ・店に行く。 ・商品を選ぶ。 ・お金を払う。 ・帰る。 } 部品「洗濯する」 { ・洗濯物を集める。 ・洗濯機に入れてスイッチオン。 ・終わったら干す。 }
↓これを組み合わせて新しい部品を書くことができます。
部品「今日すべきことをする」 { ・「洗濯する」 ・「買い物に行ってくる」 ・ご飯食べる。 ・寝る。 }
「今日すべきことをする」を実行すると、洗濯して買い物行ってご飯たべて寝る、というわけです。
● 2.3. なぜ Java 的オブジェクト指向を理解するために部品を組み合わせるということを理解しないといけないのか
部品にすると、良いことがたくさんあります。そうした良いことを積み上げていくことで、ようやくアプリケーションが出来上がります。何がどう良いのか、その理由を知ることが、Java 的オブジェクト指向を理解するための最短距離です。
1. 部品を利用するプログラムの見通しが良くなる。
↓こんな感じ。
左の方が読みやすいですね。
なぜ見通しが良くなったように感じるかというと、部品に名前がついたからです。また、短くなったからです。
見通しが良いということは、間違いに気づきやすいということです。これは良いことです。
また、部品を交換するだけで処理そのものを変えることができるということです。変更に強いというのは良いことです。
2. 同じ処理をいくつも書かなくて良くなる。
例えばある処理を 3 箇所で行いたいとき、その処理内容を 3 箇所に書かなくても、部品の実行を 3 箇所に書けば済むようになります。プログラムを書く量が短くなるのは良いことです。
また、その処理に誤りがあったとき、その修正を 3 箇所全てで行わなくても、部品の内容を修正すれば済むようになります。修正漏れを見つける手間が軽減されます。これも良いことですね。
3. 部品を利用するプログラムは、その部品の詳細を深く知る必要がなくなる。
部品の名前さえ知っていれば、その内容について詳しく知らなくても利用することができます。だから部品の名前と部品の機能概要をざっと決めておけば、部品の詳細は他のプログラマが作っても良いわけです。その代わりもっと大事なプログラミングに集中することができるようになります。複雑さが軽減されるのは良いことです。作業量が減るのも良いことですね。
Java のプログラムを作るということは、複雑な情報処理をするものを作るということです。Java のプログラムを書くということは、複雑な情報を Java でどう処理するのかを書くということです。それをプログラマという人間が読み書きするわけなので、複雑さを緩和できれば緩和できるほど、それは良い Java プログラムだということです。
Java には良いプログラムにするための仕組みがたくさん詰まっています。Java 的オブジェクト指向を学ぶということは、そうした仕組みを学ぶということです。Java 的オブジェクト指向を駆使してプログラムを書くということは、そうした仕組みの良いところを生かしたプログラミングをするということです。
3. 処理のグループとデータのグループを見つめる。
● 3.1. 処理のグループ
処理をグループにする話は前節で書いたとおりです。グループと言っても、実行順序が決まっていますのでグループという言葉は適切ではないですね。これはグループではなく、メソッドと呼びます。メソッドは用語ですので憶えて下さい。メソッドを組み合わせて Java を書くのです。
class Main { // f1 という名前のメソッド public void f1() { System.out.println( "あーいーうー" ); System.out.println( "えーおー" ); } // f2 という名前のメソッド public void f2() { System.out.println( "A〜B〜C〜" ); System.out.println( "D〜E〜" ); } // f3 という名前のメソッド public void f3() { f1(); f2(); } }
● 3.2. データのグループ
多くのメソッドはデータを扱います。データをグループにすることは良いことです。どう良いのかというと、それは先ほどと全く同じですので 2.3. を読み返しましょう。
データをグループにする仕組みが Java に搭載されています。
↓では「データのグループ」を Java のプログラムにしてみます。
// Apple という名前の「データのグループ」 class Apple { public int weight; public int price; };
● 3.3. 「メソッド」と「データのグループ」を組み合わせる
良いことだらけの「メソッド」と、良いことだらけの「データのグループ」を組み合わせましょう。
class Apple { public int weight; public int price; } class Main { // Apple を初期化するメソッド public static void initApple( Apple a, int price ) { a.weight = 100; a.price = price; } // 重さを表示するメソッド public static void printWeight( Apple a ) { System.out.println( a.weight ); } // 価格を表示するメソッド public static void printPrice( Apple a ) { int price = ( int )( a.price * 1.05 ); System.out.println( price ); } public static void main( String[] args ) { Apple a1 = new Apple(); Apple a2 = new Apple(); initApple( a1, 200 ); initApple( a2, 300 ); printPrice( a1 ); printWeight( a2 ); // ※ } }
Java プログラムならではの良い仕組みを活かせているのを確認しますね。
・メソッド「main」は、データのグループ Apple を利用しているが、Apple の詳細を知らなくても済んでいる。 ・メソッド「main」は、Apple を2個利用しているが、Apple という名前が付けられているので 2 個目も名前だけで利用できている。 ・メソッド「main」は、メソッド「initApple」と「printPrice」を利用しているが、その詳細を知らなくても済んでいる。 ・メソッド「main」は見通しが良いので、 a1 の価格を表示して a2 の重さを表示していることがよくわかる。 ・メソッド「main」の機能を、a2 の重さではなく価格を表示するように変更する場合は ※ のところを printPrice( a2 ) に交換するだけで済む。
これらが同意できないのであれば、もう一度最初から読んでください。そうしないとこの先の文章が絶望的に理解できませんので。
● 3.4. 「メソッド」と「データのグループ」が増えたときのことを想像する
プログラムの量が増えると、まずデータのグループの種類が増えます。Apple, Lemon, Peach, …と増えてゆきます。
また、メソッドの種類が増えます。initApple, initLemon, initPeach, …と増えてゆきます。
ということは、グループ化することによって複雑さを緩和したのにも関わらず、どんどんそれだけでは足りないくらい複雑になって手に負えなってゆくということです。もっと強力に複雑さを緩和する仕組みが欲しくなるんです。2.3. で書いた「良いプログラムのためのコンセプト」をもっと推し進めると良いはずです。
例えば Apple 用に initApple メソッドを、Lemon 用に initLemon メソッドを、 Peach 用に initPeach メソッドをそれぞれ用意するのなら、それぞれをまとめて部品にした方が良いはずです。
なお、部品にした方が良い理由はまたも 2.3. で書いたのと全く同じですので読み返してください。「メソッド」と「データのグループ」をひとまとめにしたものをオブジェクトと言います。オブジェクトは用語です。覚えましょう。
class Apple { public int price; public void init( int p ) { price = p; } public void printPrice() { int p = ( int )( price * 1.05 ); System.out.println( p ); } } class Lemon { public int price; public void init( int p ) { price = p; } public void printPrice() { int p = ( int )( price * 1.05 ); System.out.println( p ); } } class Main { public static void main( String[] args ) { Apple a1 = new Apple(); // Apple のオブジェクトを作る。 Apple a2 = new Apple(); // Apple のオブジェクトをもう一つ作る。 Lemon l = new Lemon(); // Lemon のオブジェクトを作る。 a1.init( 200 ); a2.init( 250 ); l .init( 150 ); a1.printPrice(); a2.printPrice(); l .printPrice(); } }
メソッド「main」では、Apple や Lemon の詳細を知らずとも書くことができる、見通しが良い、… など、さっき書いた通りの良い性質が維持されています。さらに、オブジェクトにしたことで「データのグループ」と「メソッド」の関係が掴みやすくなりました。
さて、オブジェクト a1 と a2 は同じデータグループを持ち、同じメソッドを持っています。オブジェクトの構造が全く同じですね。このように構造が同じオブジェクトがあるとき、それがどういうどういうデータを持ち、どういうメソッドと関連付けられるのか、ということを記述するのはプログラム中で一箇所にすべきです。(なぜ一箇所にすべきかという理由は再度 2.3. を読み返して考えましょう。)
この記述をクラスといいます。クラスは用語です。覚えましょう。
クラスを一つ作れば、それと全く同じ構造のオブジェクトを量産できるし、そのクラスの利用者に内部の詳細を知らせぬままプログラムを書かせることができるようになります。このクラスという仕組みは非常に強力に複雑さの緩和に役立つため、Java ではクラスをプログラムを構成する単位として据えています。
4. 「どうしたら良いクラスを作れるかを理解する」ために理解すべきこと。
● 4.1. クラスを組み合わせてプログラムを書くということ
Java では、プログラムをクラスの組み合わせで記述します。そしてあらゆるメソッドはクラスに所属します。ということは、メソッドを書く前には必ず、それがどのクラスに所属するのがふさわしいかを考えないといけないということです。
● 4.2. 見えないものをクラスにする
例えば『データ「price」から税率を含めた価格を求める』メソッドは、どのクラスに所属すべきか?を考えます。さっき書いたプログラムでは Apple と Lemon に所属していましたがこれは妥当かどうか?もっと良い場所は無いでしょうか。
僕は、こういうギモンを持つようになることが、Java 的オブジェクト指向を学ぶための最初のジャンプだろうと思っています。
Java 的オブジェクト指向を「モノをクラスに対応付けて…」と説明しているのを目にすることがありますが、その説明では物足りないような気がします。その説明では、Java 的オブジェクト指向の高みへ登ることはできないし、深くまで潜ることもできないんです、きっと。Java プログラムを構成するほとんど全てのクラスは、存在しないものに対応付けられるのが実際のところです。『データ「price」から税率を含めた価格を求める』メソッドにしても、実際に存在しないモノを表現したクラスに所属させないと良い設計に近づけないんです。
この設問でいうと例えば
・プログラム中の税関係の計算をひと通り担当する TaxCalculator クラスを定義してそこにメソッドを所属させる
というのもあり得るだろうし、
・顧客の会員ランクに応じた価格を提示する PriceProvider クラスを定義してそこにメソッドを所属させる
というのもあり得ると思います。
● 4.3. そもそもクラスを決めることは架空の構造を決めることに過ぎない
例えば Apple というクラスを定義するとして、その内部にどういうデータを抱え、どういうメソッドを所属させるのが良いか、という問いに答えはありません。価格、重さ、色、糖度、出荷日、… データを増やそうと思えばいくらでも増やせてしまいます。これではキリがない。
クラスの構造が肥大したり複雑怪奇だったら、2.3. で書いた Java 的オブジェクト指向の良いところを潰してしまうことになります。「あらゆる情報を兼ね備えた多機能な Apple クラス」を定義することは Java 的オブジェクト指向が目指す場所ではないんですね。クラスは、あくまでプログラムに必要な情報と操作を完結に表現するだけのために定義されるべきでしょう。このように、プログラムにとって都合が良いような架空の構造を紡ぎだす事をモデリングといいます。モデリングは Java 以外でも通用する用語です。覚えましょう。
● 4.4. Java 的オブジェクト指向はほとんど「クラスをどう組み合わせるか」という話に終始する
Java は、ソースコードの記述が文法的に正しいことが確認されないと実行することはできません。このチェックはクラスについても行われます。例えば、あるオブジェクトがクラスに所属していないメソッドを呼んでいたらコンパイルエラーになるし、存在しないデータ変数の値を表示しようとしてもコンパイルエラーになります。クラスを記述するということは、コンピュータに明らかな間違いを検出させるということも兼ねているんです。コンピュータに間違いを検出させる機構があるということは、それを使って検出可能な間違いが多ければ多いほど安全にプログラムが書けるということです。クラスをどう記述するとより多くの間違いを検出できるのか…、クラスをどう組み合わせるとより柔軟なプログラム設計ができるのか…、それが Java 的オブジェクト指向で語られていることのほとんどを占めます。プログラムを、上から下へのフローを軸にして考えるのではなく、クラスの組み合わせ方を軸にして考えるのが Java 的オブジェクト指向では大切です。
というわけで、ここからはクラスの話をします。
5. 「あからさまに出来が悪いクラス」を作らないために理解すべきこと
● 5.1. 情報を隠蔽することは大切だと理解する。
クラスが部品であるということは、クラスの内側とクラス外側のプログラムを分離するということです。密接に絡み合ってしまうと、クラスを利用するプログラムが変更に弱くなる可能性があります。それを避けるための、手っ取り早い作法を紹介します。
↓まずは先程のコードを再掲。
class Apple { public int price; public void init( int p ) { price = p; } public void printPrice() { int p = ( int )( price * 1.05 ); System.out.println( p ); } }
これを↓こう書き換えます。
class Apple { // public → private private int price; public void init( int p ) { price = p; } public void printPrice() { int p = ( int )( price * 1.05 ); System.out.println( p ); } // 追加 public int getPrice() { return price; } // 追加 public void setPrice( int p ) { price = p; } }
price という変数の読み書きを、getPrice(), setPrice() メソッドを通じてしか許可しないようにしました。これで、このクラスの利用者が price という名前に依存したコードを書けなくなりました。従って getPrice(), setPrice() メソッドが機能する限りは、Apple クラスの構造の変化に利用者のプログラムが引っ張られることが無くなります。例えば price という変数の名前が変わっても利用者には全く影響しないし、例えば Apple クラスから値段を保持する変数が消え去ってしまっても構わなくなりました。「Apple が price という変数を抱えている」という事実を隠蔽したわけです。
これは Java で良いクラスを作るために使う常套的なテクニックです。
● 5.2. 情報を隠蔽するために初期化が大切だと理解する。
上の例では、必ず初期化時に init() を呼ぶ決まりになっていました。もし利用者が init() を呼ばなかったら price は 0 になります。しかし、あらゆるクラスの全てのデータが必ず 0 で初期化されるというのはなかなか都合の悪い話です。絶対に 100 以上の値じゃないとダメ、というデータもあるでしょう。正常な値がセットされていないと他のメソッドの動作に著しい悪影響を及ぼす、というケースも出てくるかもしれません。こうした問題に対応するため、price の値が正常な値であるかどうかを確認するための checkPrice() メソッドを追加することができますね。しかし、そんなのをうじゃうじゃ足していったら情報を隠蔽する価値が目減りしてしまうし、クラスが複雑になります。
そこで、Java には初期化のための仕組みが用意されています。これをコンストラクタといいます。コンストラクタはクラスに所属します。
↓ Apple クラスを修正しました。
class Apple { private int price; // init() → Apple() public Apple( int p ) { price = p; } public void printPrice() { int p = ( int )( price * 1.05 ); System.out.println( p ); } public int getPrice() { return price; } public void setPrice( int p ) { price = p; } }
コンストラクタを記述すると、そもそもコンストラクタを利用しないと new ができないよう、制限がかけられます。従って、情報隠蔽を安全に開始するための処理をコンストラクタの中に書けば、必ずその通りの初期化を経たオブジェクトしか存在しないことを保証できるようになるということです。
● 5.3. 依存関係が少ないクラスは良いクラスであることを理解する。
何かクラスを記述するとき、他のクラスの名前が出てくる場合があります。
↓ 例えばこんなの。
class Apple { private int price; // 引数に Lemon クラスの名前が使われてる public Apple( Lemon l ) { price = l.getPrice(); } }
初期化の際に必ず Lemon が必要になることが分かるようになっています。これを、「Apple は Lemon に依存している」といいます。依存しているということは、例えば Lemon クラスのメソッドとかに何らかの変更があったとき、Apple クラスの内部のプログラムが影響を受ける可能性があるということです。不用意に変更箇所が増えてしまうのは悪いことですね。これを避けるためには、できるだけ「余計な依存」を避けるようにクラスを記述することが大切です。
Java プログラムは、クラスを組み合わせて作る、と書きました。そしてクラスは、何度も使いまわしたり見通しを良くするための部品としての役目を持っていることも書きました。ということはつまり、クラス間の「余計な依存」は、「プログラム全体の変更しやすさ」を著しく悪化させる原因になり得るということです。あらゆる依存は排除すればするほど良いということです。
● 5.4. インスタンスという言葉を理解する。
次のコードを考えます。
class Apple { private int price; public Apple( int p ) { price = p; } // 税込価格を表示する public void printPrice() { int p = ( int )( price * 1.05 ); System.out.println( p ); } // 税率を取得する public double getTaxRate() { return 1.05; } } class Main { public static void main( String[] args ) { Apple a1 = new Apple( 100 ); Apple a2 = new Apple( 200 ); a1.printPrice(); a2.printPrice(); } }
このとき、一度目の new によって作られたオブジェクトと二度目の new によって作られたオブジェクトはその構造は同じですが別物です。このとき、それぞれをひとつのインスタンスと言います。インスタンスは用語です。覚えましょう。インスタンスが異なるから printPrice() の実行結果も異なるというわけです。
「オブジェクト」と「インスタンス」の言葉の意味は似ていますが、オブジェクトはクラスとインスタンスを含んだ総称です。a1, a2 はそれぞれインスタンスを参照する変数であって、オブジェクトを参照しているわけではありません。
● 5.5. インスタンスに依存すべきものとそれ以外のものを区別する大切さを理解する。
さて、Apple クラスには 1.05 という数値が 2 箇所に書かれています。これは良くないので(良くない理由が分からないなら 2.3. を読みましょう)、↓改善したコードを書いてみます。
class Apple { private int price; private double taxRate; // 追加 public Apple( int p ) { price = p; taxRate = 1.05; } public void printPrice() { int p = ( int )( price * taxRate ); System.out.println( p ); } public double getTaxRate() { return taxRate; } }
この結果、taxRate は全ての Apple のインスタンスについて 1.05 で固定になります。もし 1.05 以外の値にする必要があるなら void setTaxRate( double r ) を追加する必要があるでしょう。ただしその場合、全てのインスタンスについて setTaxRate( ?? ) を呼ばないといけないということです。全てのインスタンスを追跡する仕組みを用意する必要があるということです。しかしそうすると今度はそこらじゅうのクラスに Apple クラスへの依存が発生してしまいそうです。
それもこれも taxRate が Apple のインスタンスに依存しているからです。Java には、クラスに所属しつつもインスタンスに依存しないデータを記述する仕組みが用意されています。↓コードにするとこんな感じです。
class Apple { private int price; private static double taxRate = 1.05; // static 指定 public Apple( int p ) { price = p; } public void printPrice() { int p = ( int )( price * taxRate ); System.out.println( p ); } // static 指定 public static double getTaxRate() { return taxRate; } // static 指定されたメソッドを追加 public static void setTaxRate( double r ) { taxRate = r; } }
↑このように static 指定するとインスタンスに依存しなくなります。taxRate がインスタンスに依存しないので getTaxRate も setTaxRate もインスタンスに依存しないように static 指定しました。ところでインスタンスに依存しないということは、インスタンスを作らなくても利用できるということです。例えば↓こういうコードが書けるようになります。
class Main { public static void main( String[] args ) { // インスタンスを作っていなくてもメソッドが呼べる。 System.out.println( Apple.getTaxRate() ); Apple a1 = new Apple( 100 ); Apple a2 = new Apple( 200 ); a1.printPrice(); a2.printPrice(); // 税率を変更 Apple.setTaxRate( 1.10 ); a1.printPrice(); a2.printPrice(); } }
こうしてまたひとつ、余計な依存を排除する方法が手に入りましたね。
6. クラスとクラスをより良く関係づけさせるために理解すべきこと
● 6.1. 異なるクラスに共通の構造を同一視して扱うことの大切さを理解する。
ようやく継承の紹介です。同じクラスのインスタンスは同じ構造をしているということはこれまで説明してきた通りですが、異なるクラスのインスタンスであっても部分的に同じ構造になるのが妥当である場合があります。それを記述するための仕組みが継承です。
例えば Apple にデータ「price」とメソッド「printPrice」があって Lemon にもデータ「price」と「printPrice」メソッドがあるとしたなら、その部分については全く同じ構造であるといえます。全く同じものが複数のクラスに点在しているのであれば、それは改良の余地があるということです。
この節では Apple、Lemon クラスの構造の共通部分を新しくモデリングします。たかが構造の共通部分であったとしても、逆にいえば Apple, Lemon といった部品を作るための部品ですので 2.3. で書いたような価値は相変わらず持ち得ます。また部品を作るための部品、を作るための部品、を作るための部品、… というように、階層づけられた部品化はそれぞれの階層ごとに 2.3. で書いたような価値を持つことにも留意しましょう。
ここでは共通部分をくくりだした部品に「Fruit」という名前を付けるものとします。継承を使う目的は、Apple を Fruit と同一視すること、Lemon を Fruit と同一視すること、それによって Apple と Lemon に共通する構造のみに依存したコードを書くことです。これによって、Apple に固有の部分、Lemon に固有の部分に依存したプログラムを排除します。
↓ ではコードにしてみます。
// 共通部分のクラス class Fruit { private int price; public int getPrice() { return price; } public void setPrice( int p ) { price = p; } public void printPrice() { System.out.println( price ); } } class Apple extends Fruit { public Apple( int p ) { setPrice( p ); } } class Lemon extends Fruit { public Lemon( int p ) { setPrice( p ); } } class Main { // 値段に 100 を足すメソッド。 public static void add100( Fruit f ) { int newPrice = f.getPrice() + 100; f.setPrice( newPrice ); f.printPrice(); } public static void main( String[] args ) { Apple a = new Apple( 100 ); Lemon l = new Lemon( 200 ); add100( a ); add100( l ); } }
↑ Apple クラスも Lemon クラスもすっきりして見通しが良くなりました。また add100 メソッドは、 Apple にも Lemon にも依存しないまま、双方のインスタンスの共通部分にのみ依存して処理を記述できました。さらに、データ price は Fruit の中に完全に隠蔽されました。Apple や Lemon ですら、price という名前のデータ変数に依存しない構造になったわけです。
プログラムに新しく Fruit という名前が定義されたことで、Fruit の機能を拡張すれば Apple の利用者も Lemon の利用者もその恩恵を受けることができるようになりました。一方、Apple, Lemon に続いて Peach クラスを記述することを考えると、Fruit の構造をそのまま使いまわすことが出来るうえ、Fruit にのみ依存したあらゆるプログラム部品を利用することも出来るわけです。
このように継承は、クラス間の関係を記述するうえでかなり強力な仕組みであることを理解しましょう。
● 6.2. インスタンスを作ることが許されていることの大切さを理解する。
前節で Fruit を定義しましたが Apple, Lemon と同様にこれもまたクラスですのでインスタンス化できます。
↓ ほらこんな感じで。
class Main { public static void add100( Fruit f ) { /*省略*/ } public static void main( String[] args ) { Fruit f = new Fruit(); Apple a = new Apple( 100 ); Lemon l = new Lemon( 200 ); add100( f ); add100( a ); add100( l ); } }
しかしこれは妙です。Fruit, Apple, Lemon が同列に並んでいます。Apple や Lemon を Fruit と同一視することはできましたが、Fruit を Apple や Lemon と同階層に扱うことは望んでいません。ここで有効な手立ては、Fruit だけはインスタンス化を禁止することです。これも Java に備わっている機能です。
↓ Fruit クラスをこう書き変えます。
// ↓ abstract 指定を追加 abstract class Fruit { private int price; public int getPrice() { return price; } public void setPrice( int p ) { price = p; } public void printPrice() { System.out.println( price ); } }
このように abstract をつけたクラスはインスタンス化が禁止されます。new Fruit() するとコンパイルエラーになって実行できません。従って、Fruit を Apple や Lemon と同程度に具体的で堅牢なクラスに仕立て上げる必要はなくなりました。このようなクラスを抽象クラスと呼びます。これは用語ですので覚えましょう。抽象クラスであると明示的に記述することで「それを継承したクラスのインスタンスを活用することの必然性」を利用者に伝えることにもなります。
さてこのように new を禁止しましたが、別の意味で new を禁止したい場合があります。例えばクラス A があったとして、クラス A はその内部で一度だけ A をインスタンス化したいけれど A 以外の誰によってもインスタンス化して欲しくないというような場合です。これは外部からコンストラクタへのアクセスを禁止することで対応します。
↓例えばそう、こんな風に。
class A { // コンストラクタは外部からアクセス禁止 private A() {} private static A a = new A(); public static A getUniqueInstance() { return a; } }
インスタンスが存在し得ないこととコンストラクタへのアクセスが許されないことは、意味も性質も記述も違うので、より適切な方を選択しましょう。
● 6.3. 前節のような同一視は、同一視という行為の一面に過ぎないことを理解する。
継承によって Fruit の構造が Apple や Lemon に引き継がれるのはここまで説明した通りですが、それは Fruit を継承したクラスにのみ引き継がれます。この継承の性質によってプログラムの再利用が強力に推し進められるということは、クラスを継承すればするほど再利用の幅が広がるということです。しかしそれは、Apple や Lemon が多数の継承元クラスに依存するということでもあります。継承はすればするほど良いというわけでは無いのです。
もっとクラスの継承階層に対して横断的な同一視ができると良いはずです。その仕組も Java に用意されています。それがインターフェイスです。
↓コードにしますね。
// インターフェイスの定義 interface Discountable { int getDiscountedPrice(); } // Discountable ではないクラス class Apple extends Fruit { public Apple( int p ) { setPrice( p ); } } // Discountable なクラス class Melon extends Fruit implements Discountable { public Melon( int p ) { setPrice( p ); } public int getDiscountedPrice() { return ( int )( getPrice() * 0.7 ); } } class Main { // Discountable 指定したものだけを同一視する。 public static void printDiscountedPrice( Discountable d ) { System.out.println( d.getDiscountedPrice() ); } public static void main( String[] args ) { Melon m = new Melon( 1000 ); Apple a = new Apple( 100 ); printDiscountedPrice( m ); printDiscountedPrice( a ); // コンパイルエラー。a は Discountable ではない。 } }
printDiscountedPrice メソッドは、引数 d が Discountable であることだけを要求しています。これは、Fruit を継承しているか否かには関与しないので、このメソッドは Fruit に依存しないという利点があります。Fruit と全く関係無いクラスであっても Discountable であれば printDiscountedPrice メソッドは引数 d を同一視できるのです。
● 6.4. クラスの良い組み合わせ方はプログラマたちの間で広く知られ、共有されていることを理解する。
クラスをどう組み合わせると余計な依存が排除できて、かつ情報隠蔽が徹底され、かつ柔軟であるか、ということを考えてプログラムを書いていると、よく似たパターンがあることに気づきます。そうしたいくつかのパターンに名前を付けてカタログにしたものをデザインパターンと言います。Java 的オブジェクト指向を意識してプログラムを書き続けるということは、「ここは◯◯パターンを使って設計すると良いな」という風にぴったりはまることがたくさんあるということです。
そこで、チームで開発するときなどは「ここは◯◯パターンを使っているよ」とプログラマ間で意思疎通をはかれば、クラスの組み合わせ方それ自体の見通しが良くなることが期待できます。見通しが良いことのメリットは 2.3. で書いたのと全く同じですので読み返しましょう。
7. Java 的オブジェクト指向が持つ本質的な問題を認識することの大切さを理解する
● 7.1. Java 的オブジェクト指向が問題を抱えているとしたなら…?
Java 的オブジェクト指向の良いことばかりをここまで書いてきましたが、クラスを組み合わせてプログラムを構築することに問題があるとしたらどうしましょうか。Java が提供するあらゆる仕組みが、実はその問題を推し進めてしまっているとしたら…。
Java でプログラムを書く以上、Java が提供する枠組みの外側へは出られません。したがって Java 的オブジェクト指向が根本的な問題をはらんでいるのであれば、まずそれを認識して、Java の範囲内で可能な限りの対応を検討することが大切です。
● 7.2. プログラムをクラスの組み合わせで記述することの是非
Java 的オブジェクト指向ではクラスを組み合わせて記述すること、及びクラスの構造はモデリングによって決定づけられることはここまで書いたとおりです。しかしそうやって定義したクラスというものは本当にプログラマにとって必要なものなのでしょうか。あるメソッドを利用したいだけなのに、無駄なクラスを量産していないでしょうか。過剰な抽象化、過剰な共通化、過剰な明示化によってクラスが量産されてしまいがちな性質が Java 的オブジェクト指向には潜んでいるのではないでしょうか?
良いプログラムを書くために Java 的オブジェクト指向を活用するというのに、煩雑にクラスが量産されてプログラム全体の見通しが悪くなるのであれば、それは本末転倒ですよね。例えば ◯◯Manager クラスとか、Abstract◯◯ クラスとかが多くなりすぎてしまっていませんか。
● 7.3. Java の継承という仕組みを活用することの是非
Java 的オブジェクト指向では継承の利用を前提にいくつものクラスを同一視するための共通部品をつくり、再利用を促進するなどしてプログラムを構築するということについて説明しました。継承は強力ですが、それゆえに「どこで継承元と継承先の構造を分離するのか」という判断を誤るとプログラムの質に甚大な悪影響を与える場合があります。また、Java 的オブジェクト指向では継承元は高々ひとつしか選べないという制限があります。例えば『「Apple を Fruit と同一視することが効果的な場面」と「Apple を Stock と同一視することが効果的な場面」』が共存するようなプログラムを書かないといけないことだってあるのです。
どういう手立てが良いかはケースバイケースですが、共通の部品であってもあえて継承をしない、あえて同じコードを何度も書く、そういう判断が将来のリスクを軽減する場面もあるのです。
● 7.4. 内部の構造を隠蔽するという考え方の是非
隠蔽することが何故良いのかは 5.1. で書きました。しかしもっと戻って 2.1. で「Java のプログラムは上から下へ実行する」ことも書きました。これらを組み合わせると、実装が隠蔽されたメソッドから別のメソッドを呼び、それがさらに別のメソッドを呼び… という風に永遠にメソッドを呼びつづけてしまうような事態を生む可能性があるということです。そういう誤ったプログラム構成になってしまうことを、クラスを組み合わせて動かすまで(つまり開発の終盤まで)気づかない可能性があるということです。
また一方、データを隠蔽していることは来るべきマルチスレッド環境下でのプログラミングと相性が悪いことにも留意すべきです。
8. まとめ
本稿で扱おうと思っていた説明は以上です。少しでも理解の足しになっていれば幸いです。
しつこいくらいに「Java 的オブジェクト指向」と書いてきましたが、もっと別のかたちのオブジェクト指向プログラミングも世の中にあるのです。本稿に書いてあることだけでオブジェクト指向を語るのは "井の中の蛙大海を知らず" 状態を晒すことですので控えましょうね。
でわでわ、お疲れ様でした。
- 566 http://b.hatena.ne.jp/hotentry
- 499 http://b.hatena.ne.jp/hotentry/it
- 226 http://www.ig.gmodules.com/gadgets/ifr?exp_rpc_js=1&exp_track_js=1&url=http://www.hatena.ne.jp/tools/gadget/bookmark/bookmark_gadget.xml&container=ig&view=default&lang=ja&country=JP&sanitize=0&v=66be7977a176d1dc&parent=http://www.google.co.j
- 209 http://t.co/VSpKRcYo
- 166 http://reader.livedoor.com/reader/
- 144 http://bit.ly/snGGE3
- 118 http://d.hatena.ne.jp/
- 94 http://t.co/qrz6k2CA
- 89 http://www.google.co.jp/reader/view/
- 79 http://www.google.co.jp/reader/view/?hl=ja&tab=wy
危険といってもいろんなのがあると思うんですが、どういう危険をご指摘でしょうか?