オブジェクト・プログラミング 第10回 オブジェクト指向設計のキモ
前回多かった質問(1) 削除するときに無駄なメモリが残ってしまうきがするのですが? 103 ‘よ’ 102番地 100番地 101番地 103番地 104番地 ‘お’ ‘は’ ‘ご’ ‘ざ’ 101 102 104 null Javaでは、いらないメモリを常に見張っている「ガベージコレクタ」 というすばらしい機能が標準でついています。 誰からも参照されていないオブジェクトを自動的にメモリから消します。
前回多かった質問(2) やけに簡単でした やけに難しくなりました。 是非発展問題に挑戦してください。 一番難しいところですので、しっかり復習してください。
今日考えて欲しいこと オブジェクト指向でソフトウエアを設計する上でのとても重要な考え方。 保守性 拡張性 バグのないプログラムをつくるために。 プログラマーの人的ミスでバグを生まないように。 拡張性 人の作ったプログラムをなるべく再利用できる。
今日の目標 「継承」を使った設計の利点を説明できるようになる。 オブジェクト「アクセス指定」の利点を説明できるようになる。 「拡張性」を高めるために オブジェクト「アクセス指定」の利点を説明できるようになる。 「保守性」を高めるために クラス図が読めるだけでなく、簡単なものを書けるようになる。 練習問題を書いて、提出してもらいます。
前回やったこと 第5回で配列で実装した、ItemInfoFolderを 連結リストで実装した。 →中身を替えただけ。
前回やったことのポイント ItemInfoFolderの中身を変えるだけで、配列から連結リストへ移行できた。 他のクラスを変える必要が無かった。 →クラス設計が成功した。 一つの変更をするときに、なるべく少ない個所を書き換えるだけで済むようにするのがソフトウエア設計の基本です。→拡張性 もし、あっちこっち変えなければいけなかったら大変! →バグがバグを呼ぶ。
今日の課題 配列で作ったItemInfoFolderと 連結リストで作ったItemInfoFolderの 性能比較を行ないなさい。
前回の問題点 前回の方法で、連結リストへの変更はうまくできました。 しかし、前回の方法では、2つのデータ構造を「必要に応じて使い分ける」ということが難しくなります。 今日の課題をうまくやるためにはどうしたらいいか?
前回の方法の延長で 今回の課題をクラス設計する ※ImplはImplementation(実装)の略です。
前ページのクラス設計の問題点 なぜまずいのか? VendingMachineMainに、配列と連結リストの性能比較をするための、ほとんど同じコードを2回書く必要があります。 同じコードを2回書かなければならないような設計は、ソフトウエアの保守性という観点からすると最低の設計です。 重複コードでは、一つの個所を修正するときに、あっちこっちを修正する必要が出てきます。
VendingMachineMainのリスト //まずい設計のVendingMachineMainのmainメソッド public static void main(String[] args) { StopWatch stopwatch = new StopWatch(); //ストップウオッチを作る //まず、連結リストの時間を測る ItemInfoFolderListImpl listfolder = new ItemInfoFolderListImpl(); stopwatch.start();//ストップウオッチをスタートさせる //挿入する listfolder.insert(new ItemInfo(2000,"コーラ")); listfolder.insert(new ItemInfo(3000,"ソーダ")); //.......コードが続く //次に、配列の時間を測る ItemInfoFolderArrayImpl arrayfolder = new ItemInfoFolderArrayImpl(); arrayfolder.insert(new ItemInfo(2000,"コーラ")); arrayfolder.insert(new ItemInfo(3000,"ソーダ")); } この部分は、 同じコードなので、 メソッド化したい。 しかし! 現状の設計では、 対象クラスが違う という理由で メソッド化できない。
さらに、拡張性もよくない。 本授業では取り上げませんが、HashTableというデータ構造もあります。 その場合、Mainに3回目の重複コードが加えられることになります。
そこで。。。(ここから難しいかも) ArrayImplとListImplの機能を抽象化したクラスItemInfoFolderを用意します。 え?昔に戻った? いえいえ、このItemInfoFolderは、実装を定義しないクラスです。 実装を定義しないということは、その実装が、配列か、連結リストか は知らないけれども、ItemInfoを管理するためにinsertやdeleteの 機能を持ちますよ!という意味です。
ここで継承を使います。 ItemInfoFolderArrayImplは ItemInfoFolderを継承して いるというクラス図の記号 継承を使うことによって、配列で実装したクラスも連結リストで実装した クラスも、ItemInfoFolderという共通の性質を持つことを定義できます。
継承の用語 ItemInfoFolderクラスは、 ItemInfoFolderListImplクラスと ItemInfoFolderArrayImplクラスの スーパークラス ItemInfoFolderArrayImplクラスは ItemInfoFolderクラスの サブクラス
継承とは? 異なるクラスを抽象化し、共通の振る舞いやデータ構造を共有化したスーパークラスを作ることができる。 サブクラスは、継承することによって、スーパークラスの振る舞いやデータ構造をすべて受け継ぐことができる。 今回の場合、insertやdeleteなどの振る舞いの定義だけ受け継ぎます。
継承を利用した、今日の課題の設計 VendingMachineMainクラスは、配列だか、連結リストだかは、 わからないけど、挿入、削除ができる、ItemInfoFolderを使います という設計にします。
Javaを使った継承の実装練習 まだ、何が嬉しいかは、ピンと来ないと思うので、Javaを使って、簡単な実装の練習をしてみましょう。
練習課題
Personクラスの実装 /** クラス宣言の前に * 継承の練習:人間クラス abstract宣言をします。 */ public abstract class Person { * あいさつするメソッド。 * 同じ人間でも、使う言葉によって、 * 方法は異なるのでここでは実装しません。 public abstract void greeting(); } クラス宣言の前に abstract宣言をします。 抽象クラスという意味になり、 抽象メソッドが使えます。 メソッドの返り値宣言の前に abstract宣言をします。 抽象メソッドの意味になり、 サブクラスに実装を任せます。
JapaneseImplクラスの実装 /** * 継承の練習:人間を日本人として実装したクラス */ public class PersonJapaneseImpl extends Person{ * 挨拶するメソッドの実装 public void greeting(){ System.out.println("こんにちは"); } Personクラスを 継承します! という宣言をします。
AmericanImplクラスの実装 /** * 継承の練習:人間をアメリカ人として実装したクラス */ public class PersonAmericanImpl extends Person{ * 挨拶するメソッドの実装 public void greeting(){ System.out.println("hello!"); }
PersonMainの実装 これはまずい例です。 abstractクラスはインスタンス化(new)できません。 //まずい例 public static void main(String[] args) { Person person = new Person(); person.greeting(); }
PersonMainの実装 //PersonMainクラス、メインメソッドを抜粋 public static void main(String[] args) { Person person1 = new PersonJapaneseImpl(); person1.greeting(); Person person2 = new PersonAmericanImpl(); person2.greeting(); } <実行結果> こんにちは hello!
今までの誤解 ※逆にスーパークラスを入れることはできません。 Person person1 = new PersonJapaneseImpl(); この行の意味は、Personクラスのインスタンスが入る箱をつくり、(メモリを確保し)PersonJapaneseImplクラスをインスタンス化して代入しなさい。の意。 今までは、箱には、同じ種類のインスタンスしか入らないと教えましたが、実は違いました。 訂正すると、同じ種類のインスタンスか、もしくは、サブクラスのインスタンスを入れることができるということになります。 ※逆にスーパークラスを入れることはできません。
継承の利点 mainは、サブクラスの実装がどうなっていようと、Personとして扱うことができる。 サブクラスの実装に依存しないコードが書けるため、サブクラスを増やすだけで機能を拡張できる。 public static void main(String[] args) { Person person; person = new PersonJapaneseImpl(); person.greeting();//Japaneseのgreetingが呼ばれる person = new PersonAmericanImpl(); person.greeting();//Americanのgreetingが呼ばれる }
継承のもう一つの利点 サブクラスに共通のコードを2つに書く必要が無くなる 握手をするメソッド をつけくわえたい
握手は万国共通 handShake()メソッドは、万国共通なので、Personクラスで実装してしまいます。 Personクラスを継承したすべてのサブクラスにhandShake()メソッドが継承され、使えるようになります。
拡張したPersonクラス /** * 継承の練習:人間クラス */ public abstract class Person { * あいさつするメソッド。 * 同じ人間でも、使う言葉によって、方法は異なるのでここでは実装しません。 public abstract void greeting(); * 握手するメソッド:万国共通のはず public void handShake(){ System.out.println(“握手しました”);//日本語なのは勘弁してください。 }
Personクラスの変更だけでサブクラスの機能拡張ができます。 JapaneseImpl,AmericanImplなどのクラスは、変えてません。 自動的に継承されます。 //握手をするように改造したmain public static void main(String[] args) { Person person; person = new PersonJapaneseImpl(); person.handShake();//握手しましたと表示される person = new PersonAmericanImpl(); }
握手しない人種を作りたい メソッドの再定義 (オーバーライド) をします。
NoHandShakePersonの実装 /** * 継承の練習:握手しない人種として実装したクラス */ public class NoHandShakePerson { * 挨拶するメソッドの実装 public void greeting(){ System.out.println("おっス"); } * 握手するメソッド:握手しない人種なので、再定義する public void handShake(){ System.out.println("できません");
実行結果 //PersonMainクラス、メインメソッドを抜粋 public static void main(String[] args) { Person person; person = new PersonJapaneseImpl(); person.handShake();//”握手しました“と表示される person = new PersonAmericanImpl(); person = new NoHandShakePerson(); person.handShake();//”できません“と表示される }
さて、今日の課題に戻りましょう
コードの重複部分を取り除き、メソッド化が可能になります。 //よい設計のVendingMachineMainのmainメソッド public static void main(String[] args) { //連結リスト実装の評価 performanceAssessment(new ItemInfoFolderListImpl()); //配列実装の評価 performanceAssessment(new ItemInfoFolderArrayImpl()); } //性能を調べる重複したコードをメソッド化 public static void performanceAssessment(ItemInfoFolder folder){ StopWatch stopwatch = new StopWatch(); //ストップウオッチを作る stopwatch.start();//ストップウオッチをスタートさせる //挿入する folder.insert(new ItemInfo(2000,"コーラ")); folder.insert(new ItemInfo(3000,"ソーダ")); //.......コードが続く スーパークラスを引数に とるので、どんなサブクラスでも入れられる 昔重複していた コード
オブジェクトの保守性を高める オブジェクトのカプセル化 アクセス指定
第8回で作った ItemQueueクラス 配列で実装していましたが、このコードには、保守性という面で問題があります。 //円環キューを使って商品を収納するクラス public class ItemQueue { int arrayMax = 5;//配列のサイズ Item[] itemArray = new Item[arrayMax];//商品が入る配列 int addCursol = 0;//追加カーソル int removeCursol =0;//削除カーソル //挿入するメソッド public void insert(Item insertItem){ itemArray[addCursol] = insertItem;//追加カーソルの位置に挿入する addCursol++; //追加カーソルを1増やす if (addCursol >= arrayMax){//配列の最後にきたら addCursol = 0; //ラップアラウンドする }
何故問題があるの? このQueueクラスは、以下のように使われることを想定しています。 //想定された使い方 public static void main(String args[]){ ItemQueue queue = new ItemQueue(); //最後に挿入 queue.insert(new Item(“2001/03/05”)); //先頭を削除 Item item = queue.remove(); }
不正な使い方 しかし、以下のように使われるとキューは正常な動作をしなくなってしまいます。 //不正な使い方 public static void main(String args[]){ ItemQueue queue = new ItemQueue(); //キューの最後に挿入 queue.insert(new Item(“2001/03/05”)); queue.insert(new Item(“2001/03/08”)); //動くけど不正な使い方 queue.itemArray[0] = null; //製造日03/05のItemが返ってきて欲しいのにおそらくnullが返って来る Item item = queue.remove(); }
外部からキューの配列にアクセスされると困る queue.itemArray[0] = null; この行が問題ですね。キューは、次にremove()が呼ばれたとき、何も知らずに先頭である配列の0番目を返すでしょう。 つまり、キューの中の配列は、キュー内部で操作されるべきであり、外部から参照されては、まずいことが起こる可能性があるのです。 そんなことしなければいい!とおっしゃるかもしれませんが、人的ミスというのは怖いものです。間違えない可能性は0とは言い切れません。
外部からアクセスできないようにする そのようなミスの可能性を0にする方法があります。 外部から参照されては困る変数、メソッドには、 //円環キューを使って商品を収納するクラス public class ItemQueue { private int arrayMax = 5;//配列のサイズ private Item[] itemArray = new Item[arrayMax];//商品が入る配列 private int addCursol = 0;//追加カーソル private int removeCursol =0;//削除カーソル 外部から参照されては困る変数、メソッドには、 privateアクセス修飾子を変数の前につけます
不正できなくなる //不正できない public static void main(String args[]){ ItemQueue queue = new ItemQueue(); //キューの最後に挿入 queue.insert(new Item(“2001/03/05”)); queue.insert(new Item(“2001/03/08”)); queue.itemArray[0] = null;//itemArrayにprivateがついていれば //コンパイルエラー //製造日03/05のItemが返ってきて欲しいのにおそらくnullが返って来る Item item = queue.remove(); }
Javaで使えるアクセス修飾子 public private protected 何もつけない すべてのクラスから参照可能 そのクラス内でのみ参照可能 protected そのクラスとそのサブクラスでのみ参照可能 何もつけない パッケージ内でのみ参照可能 ※パッケージについては、授業で取り上げません。 各自勉強してください。
基本的にクラスのすべての属性はprivateでよい。 現状のItemInfoクラス オブジェクトの カプセル化 という概念です。 //商品情報のクラス public class ItemInfo{ int no; // 商品番号 String name; // 商品名 //コンストラクタ public ItemInfo(int initNo, String initName){ no = initNo; name = initName; }
保守性を高くしたItemInfoクラス 商品番号と商品名への 直な参照を禁止します。 変わりに、publicで それらを得ることができる //商品情報のクラス public class ItemInfo{ private int no; // 商品番号 private String name; // 商品名 //コンストラクタ public ItemInfo(int initNo, String initName){ no = initNo; name = initName; } //商品番号を得るメソッド public int getNo(){ return no; //名前を得るメソッド public String getName(){ return name; 商品番号と商品名への 直な参照を禁止します。 変わりに、publicで それらを得ることができる メソッドを用意します。 これで、商品番号と商品名はコンストラクタで 一旦設定されたら、外部から変更できなくなりました。
外部から設定変更を許すとき 変更を許す属性にのみ publicな変更メソッドを 用意します。 めんどくさいかもしれませんが、 //商品情報のクラス public class ItemInfo{ private int no; // 商品番号 private String name; // 商品名 //コンストラクタ省略 //商品番号を得るメソッド public int getNo(){ return no; } //名前を得るメソッド public String getName(){ return name; //名前を設定するメソッド public void setName(String newName){ name = newName; 変更を許す属性にのみ publicな変更メソッドを 用意します。 めんどくさいかもしれませんが、 このポリシーは、特に複数人での作業をするときには重要です。 (オブジェクト指向は、複数人での作業を前提に考えられています。)
使い方 //第9回課題のItemInfoFolderのdisplay()から抜粋 ItemInfo iteminfo = current.data; System.out.println(“[“+iteminfo.no+”,”+iteminfo.name+”]”); //新しい使い方 ItemInfo iteminfo = current.data; System.out.println(“[“+iteminfo.getNo()+”,”+iteminfo.getName()+”]”);
今日の課題1 第8回で配列のラップアラウンドを使ったキューを実装しました。 クラス図を手書きで書いて授業時間内に提出してください。 配列でも、連結リストでも使えるように、設計しなおしなさい。 また、アクセス指定も考えて、適宜メソッドを付け加えなさい。 クラス図を手書きで書いて授業時間内に提出してください。
今日の課題2 配列で作ったItemInfoFolderと 連結リストで作ったItemInfoFolderの 性能比較を行ないなさい。 授業中に示した設計図どうり実装しなさい。 比較する項目については、各自で考えなさい。 今回は、比較が主眼ではなく、設計、実装が主眼ですので、詳細な考察は必要ありません。 スケルトンを配ります。
発展課題 さらに、ソートのアルゴリズムも柔軟に交換できるような設計図を書き、実装しなさい。
提出方法 objprog-10@crew.sfc.keio.ac.jp宛て。 今回はソースと簡単な考察を送ってください。 ソースは自分が変更したクラスをすべて送ること (感想を任意でお願いします。) Subjectはログイン名を必ず書いてください。 ex) t96844ym 余計な[]などをつけないでください。 水曜まで。