続・デザインパターン入門
1.はじめに 前回、「デザインパターンとは何か?」を学びました。「なんか難しそう」とか「エラい人 のためのものでしょ?」って思う人も多いかもしれませんが、実はそんなことないですよ ー、という話もしましたね。 デザインパターンとは「プログラムのお手本集」であり、「実は知らず知らずのうちに使っ ているもの」「オブジェクト指向の話そのもののパターンもある」といったお話もしました。 また「コーディングの経験不足を補うもの」であることもお話しました。 それからデザインパターンがいつごろ誕生したか。2000 年です。Java の誕生と同じタイミ ングです。そのとき、基本の23 パターンを提唱したのが「GoF」(Gang Of Four, 四人組) といわれる外国人4 人だという話もしました。 今回は23 のうちから代表的な 3 つに絞って、より詳しく見ていきましょう。 2.詳しく見る1-Singleton パターン Singleton パターンは並み居るパターンのうちでももっともシンプルかつよく使われるパ ターンです。 通常、クラスからインスタンスを作成する際にnew をしてインスタンスを得ますが、これ だとインスタンスがいくつでも作れます。 普通はそれでかまわないんですが、「何回作ってもまったく同じインスタンス」という場合 もあります。であれば、何個も作らずに 1 個だけ作って使いまわした方が効率がいいです ね?この仕組みをカタログ化したのがSingleton パターンです。 実装面で言うと、まず、パターンを適用したいクラスに、そのクラスのインスタンスを保 持する static なフィールドを設けます。さらに、そのクラスのインスタンスを得るための static なメソッドを実装します。多くの場合、このメソッドには「getInstance()」の名前が 付きます。getInstance()がやる内容は、前述の static フィールドの値を返却する、ただそれだけです。 コーディング例を挙げます。
public class SomeClass {
private static SomeClass someClass = new SomeClass(); public static SomeClass getInstance() {
return someClass; } } こうすると、このSomeClass クラスのインスタンスは getInstance()メソッドを呼ぶことで 得ることができます。返ってくるインスタンスは常にこのクラスが static に保持している インスタンスですので、無駄にインスタンスを作らなくて済みますね。 でも、実はこのままだと、このgetInstance()を呼ばなくても new でインスタンスが作れち ゃいます。みんながみんなこのメソッドを使ってくれるとは限らないですから、これだけ だと効果が薄いんですね。 そこで、new できないように、コンストラクタを private にしちゃいます。 public class SomeClass {
private SomeClass() { }
private static SomeClass someClass = new SomeClass(); public static SomeClass getInstance() {
return someClass; }
}
この状態だと、SomeClass を new しようとした途端 Eclipse に「コンストラクター SomeClass は不可視です」とコンパイルエラーを突きつけられます。
でもちょっとまって!インスタンスが 1 個しか必要ないなら、単にメソッドを全て static にしてしまって、インスタンス化せずに利用できるクラスにすればいいのでは? そう思ったあなた、賢いです。でも、ちゃんとSingleton パターンを適用するのには理由が あるんです。 (1) static にするとメソッドのオーバーライドができなくなってしまう。 (ア) 拡張されたクラスを作りたくても作れない。 (イ) テストするときにスタブが作りにくい。 (2) static にするとインスタンスを複数にできない(Singleton はインスタンスが1つと は限らない) つまり拡張性や保守性を考えれば、static で実装するよりも Singleton パターンで実装した 方がよいということです。
ところで余談ですが、先の例ではstatic フィールド someClass に初期値として SomeClass クラスのインスタンスを設定していましたが、実はnull で初期化しておいて、getInstance() の中でインスタンス化するほうが一般的です。次のようなコードになります。
public class SomeClass { private SomeClass() { }
private static SomeClass someClass = null; public static SomeClass getInstance() { if (someClass == null) { someClass = new SomeClass(); } return someClass;
} }
getInstance の中で someClass が null かどうか判定し、null の場合はインスタンス化を 行っています。 こうするのには理由があります。この例ではこのクラスはとてもシンプルですが、もっ と複雑なクラスだった場合、あるいはインスタンス化の手続きが重い(時間のかかる処 理がたくさんある)場合だと、このクラスが参照された瞬間にインスタンス化が実行さ れてしまうのはよくないからです。 利用するときになって初めてインスタンス化が行われたほうが、マシンにかかる負荷の 点でも、起動時に利用者を待たせるかもしれない可能性を考えても良いということにな ります。
【問題】 (1) 内部に StringBuilder のインスタンスを保持していて文章生成を行うクラス、 「SenetenceGenerator」を用意しました。またそのクラスを利用して文章を生成 するmain メソッドを持つクラス Main も用意しました。しかし、現状は結果と して空文字列が表示されてしまいます。Singleton パターンを適用して、正しく 文章が表示されるようにしましょう。 3.詳しく見る2-Iterator パターン
Iterator パターンは、JDK に「Iterator」というインタフェースがあるので Java のみな さんおなじみかと思います。.NET の方なら「Enumerator」といえばおわかりいただけ るでしょうか。
とりあえず例を見て見ましょう。以下のような「おもちゃ」を表現するための「Toy」ク ラスがあるとします。
package toybox.easyversion; public class Toy {
private String name; public Toy(String name) { this.name = name; }
public String getName() { return name;
} }
このToy クラスのインスタンスの集合体である「ToyBox」クラスがあるとします。 package toybox.easyversion;
public class ToyBox {
private Toy[] toys = new Toy[256]; private int last = 0;
public Toy get(int index) { return toys[index]; }
public void add(Toy toy) { this.toys[last] = toy; last++;
}
public int getLength() { return last;
}
public ToyBoxIterator iterator() {
ToyBoxIterator it = new ToyBoxIterator(this); return it; } } このおもちゃ箱クラスは 256 個までおもちゃインスタンスを格納できます。なお、簡単 のため判定処理が入っていないので257 個めを入れようとするとエラーになります。 そしてこのクラスには反復子(イテレータ)のインスタンスを得るためのメソッド、 「iterator()」があります。 反復子は以下のようなメソッドで構成されます。 hasNext() メソッド…次のオブジェクトが存在するか否かを boolean 値で返す next() メソッド…次のオブジェクトを取得しつつ、インデックスを前に進める
package toybox.easyversion; public class ToyBoxIterator { private ToyBox toyBox; private int index;
public ToyBoxIterator(ToyBox toyBox) { this.toyBox = toyBox;
this.index = 0; }
public boolean hasNext() {
boolean result = index < toyBox.getLength(); return result;
}
public Toy next() {
Toy toy = toyBox.get(index); index++; return toy; } } またフィールドとしておもちゃ箱インスタンスを保持します。hasNext() や next()のメ ソッドはこのインスタンスについて次の有無を確認したり取り出して返したりします。 このクラスを群を利用して複数のおもちゃインスタンスを管理すると、次のようなコー ドになります。 package toybox.easyversion; public class Main {
public static void main(String[] args) {
// おもちゃをおもちゃ箱に4つ格納
ToyBox toyBox = new ToyBox(); toyBox.add(new Toy("ヨーヨー")); toyBox.add(new Toy("おはじき")); toyBox.add(new Toy("ぬいぐるみ")); toyBox.add(new Toy("剣玉")); // おもちゃ箱インスタンスからおもちゃ箱反復子(イテレータ)を得る ToyBoxIterator it = toyBox.iterator(); // 次がある間取り出して名前を表示する処理を繰り返す while (it.hasNext()) { Toy toy = it.next();
System.out.println(toy.getName()); }
} }
こうしておくと、この繰り返し処理は ToyBox クラスの実装に依存しなくなる、つまり ToyBox クラスをどんなに変更しようが、この繰り返し部分の変更が不要となります。
【問題】
Toy インスタンスを配列ではなく List で管理するように ToyBox クラス
とToyBoxIterator クラスの実装を変更し、繰り返し処理に変更が必要ないことを 確認しましょう。 さて、上記に示した例は簡単のため、シンプルに書きましたが、実際にはもっと汎用性 を高めるため、インタフェースを使用して表現するのが普通です。 まず、集合体となるクラス─上記の例では ToyBox クラスですが─が、iterator()メソッ ドを持つようにインタフェースを用意し、ToyBox クラスに実装させます。 package toybox;
public interface Aggregate {
public abstract Iterator iterator(); }
package toybox;
public class ToyBox implements Aggregate { private Toy[] toys = new Toy[256]; private int last = 0;
public Toy get(int index) { return toys[index]; }
public void add(Toy toy) { this.toys[last] = toy; last++;
}
public int getLength() { return last;
}
public Iterator iterator() {
Iterator it = new ToyBoxIterator(this); return it;
} }
「Aggregate」とは「集合体」という意味です。何かの集合体であるクラスにはこのインタフ ェースを実装させる、というルールにすれば必然的に Iterator を返すような iterator()メソ ッドを実装することになります。 次に、Iterator インタフェースを用意して反復子クラス─例では ToyBoxIterator クラスです ─に、実装させます。Iterator インタフェースはメソッドとして「next()」と「hasNext()」 を実装することを要求します。 package toybox;
public interface Iterator {
public abstract boolean hasNext(); public abstract Object next(); }
package toybox;
public class ToyBoxIterator implements Iterator { private ToyBox toyBox;
private int index;
public ToyBoxIterator(ToyBox toyBox) { this.toyBox = toyBox;
this.index = 0; }
public boolean hasNext() { boolean result = false;
if (index < toyBox.getLength()) { result = true;
}
return result; }
public Object next() {
Toy toy = toyBox.get(index); index++; return toy; } } このようにすることで、例えばこれが ToyBox クラスではなく全然別な集合体を扱う場合でも、 同じように扱うことができます。
main のクラスは次のようになります。
package toybox; public class Main {
public static void main(String[] args) {
// おもちゃをおもちゃ箱に4つ格納
ToyBox toyBox = new ToyBox(); toyBox.add(new Toy("ヨーヨー")); toyBox.add(new Toy("おはじき")); toyBox.add(new Toy("ぬいぐるみ")); toyBox.add(new Toy("剣玉")); // おもちゃ箱インスタンスから反復子(イテレータ)を得る Iterator it = toyBox.iterator(); // 次がある間取り出して名前を表示する処理を繰り返す while (it.hasNext()) { Toy toy = (Toy)it.next();
System.out.println(toy.getName()); } } } ToyBox クラスをインスタンス化して Toy インスタンスを詰め込むところは一緒です。 next()メソッドが汎用化のために戻り値の方が Object になったことで、キャストが必要にな ってしまいましたがあとは一緒です。 【宿題】 実際にこのクラスを作成しましょう。 実は続きがあります。汎用化したものをさらに使いやすくするため、JDK5.0 以降に導入され ている「ジェネリクス宣言」を使うと、集合体に何のクラスのインスタンスが入るのかを指 定することができるようになります。
普段 Java や.NET でコーディングしていると「List<String>」だのと書くこともあると思いま すが、あれです。
ここでは基本をおさえるということで、そこまではやりませんが頭の片隅にいれておいてい ただけるといいかと思います。
4.詳しく見る3-FlyWeight パターン 3つめは FlyWeight パターンを見て見ましょう(前回とまったく同じでは面白くないの で:-p)。 FlyWeight というのはボクシングでいう「フライ級」のことです。コーディングにおいて はなにをフライ級にする(=減量する)かというと、メモリに保持するインスタンスで す。 例を見て見ましょう。 /** * インスタンスを1つ作るのにとても時間とメモリを食うクラス */
public class HeavyObject { private Object heavyData; public Object getHeavyData() { return heavyData; } /** * コンストラクタ。引数を元にして、フィールドの値を設定する。 * @param seed */
public HeavyObject(String seed) {
this.heavyData = generateHeavyData(seed); } /** * 何か重い処理をして、メモリを食うような重い結果を生成して返すメソッド。 * @param seed * @return */
private Object generateHeavyData(String seed) {
// ここで何か重い処理をして、メモリを食うような重い結果を生成 try { Thread.sleep(1000); } catch (InterruptedException e) { } // 重い物を返す(イメージ) return seed; } } 上記は、インスタンスを作るのにすごく時間がかかって、かつメモリを大量に消費す るクラスです。簡単のために「重い重い」といいつつシンプルな処理になっていて、 時間がかかるといっても「sleep」しているだけですが(笑)、実際の現場の開発では 本当に時間とメモリを消費するようなクラスは存在します。 例えば計算に時間がかって、かつ結果データがすごく大きい、などです。
このクラスのインスタンスをいくつか作る処理があるとします。
public static void main(String[] args) { HeavyObject objT = new HeavyObject("T"); HeavyObject objH = new HeavyObject("H"); HeavyObject objI = new HeavyObject("I"); HeavyObject objS = new HeavyObject("S"); HeavyObject objI2 = new HeavyObject("I"); HeavyObject objS2 = new HeavyObject("S"); HeavyObject objA = new HeavyObject("A"); HeavyObject objP = new HeavyObject("P"); HeavyObject objE = new HeavyObject("E"); HeavyObject objN = new HeavyObject("N"); System.out.println(objI); System.out.println(objI2); } アルファベットを1文字ずつコンストラクタに渡し、10 個のインスタンスを作っています。 1 個のインスタンスを作るのに 1 秒かかるとすると、合計 10 秒かかります。 また、1 個のインスタンスがメモリを 1 メガバイト消費するとすると、合計 10 メガバイト のメモリが消費されます。 最後に変数 objI と objI2 を表示していますが、コンストラクタに渡す引数は同じでも当然 違うインスタンスなので、違う値が表示されます。 【出力例】 mysample.HeavyObject@26db62 mysample.HeavyObject@10d0630 しかし、objI と objI2 が持っている内部データはどう考えても同じのはずです。だから、 インスタンスを2つ作ってしまうのはムダ。 では次のようなコードだったらどうでしょう?
public static void main(String[] args) {
HeavyObject objT = HeavyObjectGenerator.getHeavyObject("T"); HeavyObject objH = HeavyObjectGenerator.getHeavyObject("H"); HeavyObject objI = HeavyObjectGenerator.getHeavyObject("I"); HeavyObject objS = HeavyObjectGenerator.getHeavyObject("S"); HeavyObject objI2 = HeavyObjectGenerator.getHeavyObject("I"); HeavyObject objS2 = HeavyObjectGenerator.getHeavyObject("S"); HeavyObject objA = HeavyObjectGenerator.getHeavyObject("A"); HeavyObject objP = HeavyObjectGenerator.getHeavyObject("P"); HeavyObject objE = HeavyObjectGenerator.getHeavyObject("E"); HeavyObject objN = HeavyObjectGenerator.getHeavyObject("N"); System.out.println(objI); System.out.println(objI2); } 「HeavyObjectGenerator」というインスタンスを生成するためのクラスが登場しますが、 それは例えばこんなコードで書かれます。 package mysample; import java.util.HashMap; import java.util.Map; /** * HeavyObjectインスタンスを作るクラス */
public class HeavyObjectGenerator { private HeavyObjectGenerator() {}
private static Map<String, HeavyObject> instancePool = new HashMap<String, HeavyObject>();
public static HeavyObject getHeavyObject(String s) { if (!instancePool.containsKey(s)) {
instancePool.put(s, new HeavyObject(s)); } return instancePool.get(s); } } instancePool というフィールド変数がインスタンスを保持しておくプールになっていて、 getHeavyObject()メソッドにおいてはすでに存在するインスタンスであればそこから引っ 張りだされて返却されます。もし存在しなければ、新たに new されます。 main メソッドが実行されると、次のように表示されます。 【出力例】 mysample.HeavyObject@921a90 mysample.HeavyObject@921a90
つまり、objI と objI2 はまったく同じインスタンスです。
▲ 同じものは1つずつで十分!
【問題】
実際にこのクラスを作成しましょう。
その際、実際に速度の差がわかるように、main メソッドは次のように書きましょう。
public static void main(String[] args) { long start = System.currentTimeMillis(); // (インスタンス取得・表示処理)
long end = System.currentTimeMillis(); System.out.println((end - start) / 1000); } 5.まとめ 今回は 23 のパターンのうち、3つにしぼって少し詳しく解説しました。 たった3つですが、少しでもデザインパターンを知ることの価値、意義を感じていただけ たら幸いです。 次回は、11 月上旬を予定しております。実際の現場の開発ではどういった面に使われてい るのかを学べたらいいなと考えて鋭意ネタを仕込んでおります。乞うご期待。 【今日のソースファイル】 https://docs.google.com/file/d/0B9MQQemGri2WLU96RjlOUlBYSEE/edit?usp=sharing T I I S S H T I S H