Deep Side of Java〜Java 言語再入門 第3回 〜 クラス設計とデザインパターン

やさしいデザインパターン

Singleton






Singleton

ある程度以上規模の大きなアプリを開発する時には、ユーザによるカスタマイズを柔軟に出来るようにすることは多い。その時にそのアプリで共通して使うプロパティを設定し、各サブシステムでそれを使うたびにプロパティを参照して、実際の値を取得したいことが多い。つまり、プロパティはどんなクラスからも見えなくてはならないのである。

これを実現するのには、ちょっと工夫が要る。次のようなやり方が考えられるが、あまり良い方法とは言えない。

  1. メインクラスでプロパティを設定し、そのインスタンスを各クラスのコンストラクタに引数で渡し、各クラスのインスタンス変数に格納する。→ 小規模なクラスでインスタンスを大量に作成するものでは不効率である。また、そのプロパティを参照する可能性のあるクラスを呼び出すクラスに、すべて引数で渡さなくてはならなくなる。

  2. メインクラスの public なクラス変数にセットし、MainClass.propColorA のように参照する。→ プロパティを利用するクラスが、メインクラス名に依存することになり、再利用性が大幅に低下する。これはクラスメソッドとしてプロパティを取得するように実装しても変わらない。

このジレンマは匿名のかたちで、メインクラスで生成されたプロパティのインスタンスを取得するのが難しいことにある。そこでこの Singleton デザインパターンを利用する。Singleton の定義は、public なクラスであるが、そのインスタンスはただ1つしかなく、何度取得しようとも常に同じインスタンスが返ってくることである。これはクラス変数とクラスメソッドをうまく使って実装する。アプレット用の現実的なプロパティ・クラスを実装してみよう。

import java.util.*;
import java.applet.*;
import java.io.*;

public class AppletProperty {
     /* private なクラス変数として、実際のデータを格納する Properties クラス
        のインスタンスを定義する。いわゆる「委譲」である。 */
     private static Properties prop = null;
     /* private なクラス変数として、自分自身のインスタンスを保持する。*/
     private static AppletProperty instance = null;
 
     /* コンストラクタは private であり、外部から呼び出すことができない。
        これはインスタンスが唯一であることを保証する。*/
     private AppletProperty() {
          prop = new Properties();
     }

     /* インスタンス生成に public なクラスメソッドを使う。*/
     public static AppletProperty getInstance() {
          if( instance == null ) { 
                /* 初めての呼び出しの時は、インスタンスを生成して保持する */
                instance = new AppletProperty();
          }
          /* だからクラス変数として保持されている唯一のインスタンスが常に返る。*/
          return instance;
     }

     /* 以降、ほとんど java.util.Properties クラスのメソッドのアダプタ */
     public void load( InputStream is ) throws IOException { 
          prop.load( is ); 
     }

     public void store( OutputStream os, String header ) throws IOException {
          prop.store( os, header );
     }

     public Enumeration propertyNames( ) { 
          return prop.propertyNames(); 
     }

     public String getProperty( String key ) { 
          return prop.getProperty( key );
     }

     public String getProperty( String key, String def ) { 
           return prop.getProperty( key, def );
     }

     /* 独自の追加メソッド 整数値プロパティを返すコンビニエンスメソッド */
     /* 例外を投げると面倒なので、デフォルト値を第二引数に取る */
     public int getIntProperty( String key, int def ) {
          int ret;
          String s = prop.getProperty( key );
          if( s == null )  return def;
          try {
               return Integer.parseInt( s );
          } catch( NumberFormatException e ) { return def; }
     }

     /* 独自の追加メソッド 真偽値プロパティを返すコンビニエンスメソッド */
     /* 文字列値との対応関係は適当に .... */
     public boolean getBooleanProperty( String key ) {
          String s = prop.getProperty( key );
          if( s == null ) { return false;
          } else if( s.equals( "true" ) ) { return true;
          } else if( s.equals( "yes" ) ) { return true;
          } else {
               try {
                    if( Integer.parseInt( s ) == 1 ) return true;
                    return false;
               } catch( NumberFormatException e ) { return false; }
          }
     }

     public void setProperty( String key, String val ) {
       /* 間抜けな話だが、Properties.setProperty(String,String) はJDK 1.2 で初めて
        サポートされている。アプレットはIE対策を考えなくてはならないので、JDK 1.1
        レベルでないと安全ではない。だから、Properties クラスが継承する Hashtable
        クラスのメソッドを使う。*/
          prop.put( key, val );
     }

     /* さて、アプレットに特化したコンビニエンスメソッドである。Applet を引数に取り、
        getParameter() によって <param 〜>タグの内容を取得し、それをプロパティに
        セットする。とはいえ、Applet クラスにすべての param を列挙するインターフェイス
        実装がないので、あらかじめ存在する可能性のあるプロパティ名をプロパティにセット
        しておいて、同じ名前の param があれば、それを値として採用する。*/
     public void setProperty( Applet app ) {
          String key, val;
          Enumeration e = prop.propertyNames();
          while( e.hasMoreElements() ) {
               key = (String)e.nextElement();
               val = app.getParameter( key );
               if( val != null ) {
                    setProperty( key, val );
               }
          }
     }
}
public class MyApplet extends Applet {
     /* メインクラスのインスタンスとして保持しておく */
     AppletProperty ap = AppletProperty.getInstance();
     /* 初期値を与える文字列。こういう風に書いておくと Xの fallback_resourece 風で
        あり、可読性が良い */
     static String initProp = "myName = \n" +
                              "value = 10\n" +
                              "debug = true\n";
     MyPanel mp;  /* 適当なサブクラス */

     public void init() {
          /* まず初期値をプロパティにセット */
          byte [] initb = initProp.getBytes( );
          InputStream is = new ByteArrayInputStream( initb );
          try {
               ap.load( is );
          } catch( IOException e ) { /* 具体的なエラー処理 */ }
          /* そして、param の内容で更新 */
          ap.setProperty( this );
          /* サブクラスにプロパティを渡さなくても良い */
          mp = new MyPanel();
     }
     ...........................
}

public class MyPanel extends Panel {
     private String name;
     private int value;
     private boolean debug;

     public MyPanel( ) {
          /* 引数でなくても、こうやって唯一のインスタンスを得ることができる */
          AppletProperty ap = AppletProperty.getInstance();
          /* 実際の値の参照 */
          name = ap.getProperty( "myName" );
          value = ap.getIntProperty( "value", 10 );
          debug = ap.getBooleanProperty( "debug" );
     }

あるいは、アプレットではなくて、スタンドアロンのアプリでも、リソースファイルを読んでプロパティ値を設定できる。

     AppletProperty ap = AppletProperty.getInstance();
     private void init() {
          try {
               ap.load( new FileInputStream("GoServ.rsc") ); 
          } catch( Exception e ) {
               System.out.println( e );
               System.exit( 1 );
          }
          port = ap.getIntProperty( "Port", 5555 );
          connTimeout = ap.getIntProperty( "connectTimeout", 100 );

当然、Singleton の別な応用として、そのアプリ全体で唯一のインスタンスを取得するために使うことが出来る。通信ポートなど排他的にしか使えないリソースを管理する、あるいはマルチスレッドで書き込みを行う最終的なデータオブジェクトを、Singleton にすることが出来る。この場合、synchronized 文でうまく mutex に配慮しておく必要があるのは当然である。(ここらへんの事情は「スレッドって何?」を参照のこと)

Singleton パターンの問題点

便利なものには「裏」があるのは、世の中の一般原理である。この Singleton パターンにもやはりいろいろな問題点がある。一部の人々は「Singleton は邪悪である!」とまで言っているようでもある。その理由、つまり Singleton を使う上で問題になる点を指摘し、Singleton パターンを「骨までしゃぶって」みよう。これらの実装コードは、「対戦型五目並べ」の中にあるので、具体的にはそちらで確認されたい。

  1. マルチスレッドに対応するのが素直にいかない。たとえば、アプレットはブラウザが起動する1つのJVMによって、「マルチスレッドで」起動される。これは複数のアプレットで同一のSingletonプロパティクラスを使っている場合に、まったく別のアプレット用のプロパティであるにも関わらず、後で起動されたアプレット用のプロパティによって、先に起動されたアプレット用のプロパティが上書きされてしまう。要するに「Singleton = JVMの中での共有」なのだが、「共有が広すぎる!」ケースが起きてしまうだ。だから、これは何らかの手段によって、プロパティ同士を区別する必要が出る。具体的には getInstance メソッドに、「プロパティを区別するキー」を渡し、プロパティクラスではインスタンスを特定するのに、クラスメンバである Hashtable から検索して返す、というくらいのことをする必要がある。
    public class Singleton {
       /* クラスメンバは Hashtable  */
       static private Hashtable hash = new Hashtable();
       static public getInstance( Object key ) {
          Singleton s = hash.get( key );
          if( s == null ) { /* key に対応するインスタンスがまだない */
              s = new Singleton();  /* 作成し、Hashtable に登録 */
              hash.put( key, s );
          }
          return s;
       }    
    
  2. その時、検索キーをうまく設定してやらないと、意味がない。たとえば検索キーとしてアプレットを特定する文字列を使う、という考え方がある。これはこれでうまく動作するようである....ただし、同一のアプレットでプロパティ内容の異なるものを、同一のブラウザで表示するまでは。このケースでは「キーの文字列」は2つのアプレットで同一なので、互いに区別できないのだ(Hashtable は Object.equals() で比較するから、同じ「内容」の文字列ならば、インスタンスとしては異なっていても一致する)。このケースでは、キーとして文字列ではなくて、何かのインスタンスを渡してやる(まあ、アプレット自体のインスタンスが適切かな?)必要がある。しかしこれは、利用局面ではこういうことになる。「アプレットのインスタンスを検索キーにするわけだから、それを利用局面のクラスに情報として渡してやる必要がある!」 だったら素直にアプレット・クラスにプロパティを置いてやればよいのであり、Singleton を使う必然性がなくなってしまうのである! 要するに、「利用局面を全体のコンテキストから切り離す」ために Singleton を使ったのだから、これでは「堂々巡り」である。この馬鹿な状況を回避するには、検索キーの「別名付け」を可能にするのも工夫である。
      /* キーに「別名」をつける。一つのキーだけでは使いづらい時に有効。*/
       public static void alias( Object src, Object dest ) {
          Singleton sp, ret;
          sp = (Singleton)hash.get( src );
          if( sp == null ) {
             sp = new Singleton();
             hash.put( src, sp );
          }
          hash.put( dest, sp );
       }
    
  3. 派生クラスが作りにくい。派生クラスを作る場合に、ちょっと配慮が要るのである。なぜならば、getInstance() メソッドは「クラスメソッド」であり、「クラス名を明示して使う」わけである。だから、クラスメソッドの所属(基底クラス)と、実際に生成されるオブジェクトの所属(派生クラス)が異なる、というちょっと捻ったことになる。
        MySingleton ms = (MySingleton)BaseSingleton.getInstance();
    
    ホントこういう感じに呼び出したいのだが、これを実現するには工夫が要る。要するに、基底クラスに「生成すべき派生クラスが何であるか」という情報が、あらかじめ渡ってなくてはならないのである。こんな具合に動的ローディングと組み合わせるのが良いだろう。
    public class BaseSingleton {
       private BaseSingleton instance = null;
       /* 最初の呼び出しに使う */
       static public BaseSingleton getInstance( String subclass ) 
                                                  throws Exception {
          Class cls = Class.forName( subclass );
          instance = (BaseSingleton)cls.newInstance();
          /* 生成されたインスタンスは派生クラスのもの */
          return instance;
       }
    
       /* 二回目以降の呼び出しに使う */
       static public BaseSingleton getInstance( ) {
          return instance;
       }
    
    一応これでうまくいくのだが、初回呼び出しと二回目以降の呼び出しでインターフェイスが異なるのが、Singleton の本旨に若干反している.... まあ、これは「getInstance() で使いたい!」という強力な動機(生成局面と利用局面の結合を避けたい)がなければ、メリットは薄いわけである。

  4. ガベージコレクタが誤解する可能性。ガベージコレクタによる動的なインスタンス破棄をするケース(Java はそうだ)では、ガベージコレクタは「現在、生きているオブジェクトをマークし、全体のオブジェクトの中でマークされていないオブジェクトを破棄する」というロジックで動作する。Singleton では、特に利用局面でインスタンスを保持しなくても使えてしまう。言い替えれば、「うっかりメソッド内のローカル変数だけで定義してしまう」ことがある。もし、ガベージコレクタが起動したときに、「他のクラスによってインスタンスが保持されていない状態」ならば、そのインスタンスは回収されてしまう可能性があるのである。これは「ローカル変数だけで定義している」ケースで起きる可能性がある。だから、「オマジナイとして、どこかのクラスで、インスタンスメンバとして、Singleton インスタンスを保持しておく」必要がある。ちなみに、Singleton クラス内部で保持されている自分自身は、一種の「循環参照」だとガベージコレクタは捉えるようである...だから、別なクラスでのインスタンスでなくてはならない理由がある。



copyright by K.Sugiura, 1996-2006