シングルトンの賢い使用法

シングルトンを使用する場合と使用しない場合を見分ける

シングルトンは使われ過ぎでしょうか?ベテランのプログラマーであるJ. B. Rainsbergerは、シングルトンが過度に使用されている可能性があることを示し、その理由を説明した上で、シングルトンを使用する場合と、より柔軟な選択肢を探す場合とを見分けるためのヒントを提供しています。

J.B. Rainsberger, Software Developer, IBM Toronto

J. B. Rainsbergerは、オンタリオ州マーカムにあるIBMトロント・ラボのソフトウェア開発者です。トロント大学コンピューター・サイエンス修士課程に在籍し、1998年以降E-commerce Development Organizationに勤務しています。彼は、ルール・ベースの個別設定を行う作業が忙しくない時に、コードのシンプルで抜本的な改良方法を探しています。変化の多いe-commerce業界における絶え間ない変化に対して、自分の周囲の組織が迅速かつ冷静に対応できるような方法を、ゆっくりとしかし着実に模索しています。



2001年 1月 07日

プログラミング・コミュニティーは、グローバル・データとグローバル・オブジェクトの使用を思いとどまらせようとしています。しかし、アプリケーションは依然として、あるクラスの単一インスタンスと、そのクラスにアクセスするためのグローバルな方法を必要としています。その一般的なソリューションが、シングルトンとして知られるデザイン・パターンです。しかし、シングルトンのテストは必要以上に困難であり、またシングルトンはそれを使用するアプリケーションに対し、強い制約を課する場合があります。この記事では、シングルトン・パターンの使用が適切でない実際かなりあるケースに適用できる回避策について説明します。また、本当にシングルトンであるべきクラスの特性についても説明します。

自動ユニット・テストは以下のような場合に非常に効果的です。

  • クラス間の結合度が、必要なだけの強度に抑えられている
  • 協調動作するクラスに関し、本番実装の代わり模擬実装を使用するのが容易である

確かに、クラスが疎結合の場合は、単一クラスを個々にテストすることに専念できます。クラスが密結合の場合は、クラスのテストはグループごとによってのみ行われるため、バグの分離がより困難になります。一般的には、各契約以外には仮定を置かずにクラスが協調動作する場合、テストが最も簡単です。

ユニット・テストでは、各クラスがシステムの他の部分から切り離した状態で、要求のとおりに振る舞うことを確認します。ユニット・テストをより効果的なものにして、それをより迅速に実行するための 一般的なテクニックの1つは、協調動作するオブジェクトの本番実装の代わりに模擬オブジェクトを使用することです。たとえば、クラスB が例外をスローした際にクラスA がどのように反応するかをテストするには、リスト1に示されているようなコードを記述すれば十分です。

リスト1. 模擬オブジェクトを使用したコード例
public class MyTestCase extends TestCase {
     ... 
     public void testBThrowsException() {
         MockB b = new MockB(); 
         b.throwExceptionFromMethodC(NoSuchElementException.class); 

         A a = new A(b);  // Pass in the mock version
         try {
             a.doSomethingThatCallsMethodC(); 
         }
         catch (NoSuchElementException success) {
             // Check exception parameters? 
         }
     }
     ...  
}

振舞いの再作成よりもその振舞いをシミュレートするほうがはるかに簡単です。そこで、クラスBの本番実装が実行するシナリオの再作成ではなく、NoSuchElementException をスローするメソッドc() をシミュレートすることにします。シミュレーションは、シナリオ の再作成ほどクラスBに関する専門知識を必要としません。

多くを知りすぎるシングルトン

I know where you live アンチパターンは、シングルトンを多用するアプリケーションで散見される実装アンチパターンです。協調動作するクラス間で、クラスが他のクラスのインスタンスを獲得する場所を知っている場合にそれが発生します。

どこに害を及ぼすのでしょうか?クラスが協調動作するクラスのインスタンスを獲得する場所を知っている場合に、クラス間の結合度が大幅に増大されます。まず、サプライヤー・クラスのインスタンス作成方法における変更がクライアント・クラスに伝播してしまいます。これはLiskov Substitution Principle に違反します。このPrincipleは、クライアント・クラスに対して、サプライヤーのどんなサブクラスとも協調動作できる自由度をアプリケーションに許可するものです。この違反はユニット・テストの際に問題が認識されますが、より重要なことは、下位互換性を保つ形でのサプライヤーの拡張が難しくなることです。まず上述のように、ユニット・テストでは、サプライヤーの振舞いをシミュレートすることを目的として模擬サプライヤー・インスタンスをクライアント・クラスへ渡すことができません。次に、クライアント・コードを変更せずにサプライヤーを拡張することはできません。また、その場合には、サプライヤーのソースへのアクセスを必要とします。たとえサプライヤーのソースへアクセスできたとしても、サプライヤーに連なる178すべてのクライアントを本当に変更したいと思いますか?リラックスした楽しい週末を過ごしたいと思いませんか?

協調動作するクラスは、アプリケーションがそれらの実際の結合を決定できるように構築されるべきです。これによってアプリケーションの柔軟性が増大し、ユニット・テストはよりシンプルで迅速に、また通常はより効果的に行えます。クラスのテストが簡単になればなるほど、開発者も頻繁にテストを行うようになるでしょう。


シングルトンからの移行

シングルトンは最初に考えていたほど望ましいものではないようなので、クライアントがサプライヤーがシングルトンであることを意識しないように、クライアントのコードを効果的に作成する方法を説明したいと思います。

では、問題について詳しく説明します。クライアントが、シングルトンのインスタンスを取り出すときはいつでも、そのサプライヤーがシングルトンであるという事実に必要もなく結びつけられます。たとえば、DeployerDeployment について考えてみましょう。アプリケーションが必要とするのは1つのDeployer のみであるため、それをシングルトンにします。これで、リスト2で示されているようにこのメソッドのコード化ができるようになります。

リスト2. Deployerのコード化
public class Deployment {
     ... 
     public void deploy(File targetFile) {
         Deployer.getInstance().deploy(this, targetFile); 
     }
     ...  
}

これは簡単でよさそうな方法のように見えます。なぜなら、クライアントは単にDeployment にそれ自身の配置を要求するのみで、Deployer に関する知識は必要ありません。これは確かに事実ですが、その利点は、異なる種類のDeployer を使用しなければならない (あるいは使用したい) 場合の結果として、すぐに覆されてしまいます。Deployment は具体的なクラスであるDeployer について知っているため、Deployment のソースを変更せずにDeployer のサブクラスを置き換えることはできません。

リスト3のように、Deployer がシングルトンであることを知っているDeploymentではなく、クライアントがDeployment のコンストラクターへDeployer インスタンスを渡すべきです。

リスト3. DeploymentのコンストラクターへのDeployerの送付
public class Deployment {
     private Deployer deployer; 

     public Deployment(Deployer aDeployer) {
         deployer = aDeployer; 
     }

     public void deploy(File targetFile) {
         deployer.deploy(this, targetFile); 
     }
     ...  
}

この2つのクラスは前ほど密結合ではありません。これで、Deployer が作成される方法に依存するのでない、Deployment からDeployer へのシンプルな関連ができました。そしてアプリケーションにその決定を行わせることができます。

古いコードでは、クライアントが変更を行わなければなりません。インターフェースには静的メソッドを置くことはできません。新しいコードでは、Deployment は、変更を必要としません。その代わりに、Deployment のクライアント、つまりアプリケーションが、アプリケーションにとって適切な方法で変更を行います。さらに、アプリケーションのデザインが優れていれば、1つの変更によって、アプリケーションのすべてのDeployment インスタンスの振舞いを変更できるでしょう。これで、Deployer がインターフェースである必要がでてきたとして、来週か来月か来年かはわかりませんが、問題はありません。

ユニット・テストにも利点があります。複数のテスト・ケースの実行については、テスト・ケースはそれぞれ少しずつ異なるDeployer が必要となる場合があります。通常、Deployer は特定の振舞いをシミュレートするためにある状態であることが必要です。前述のように、これを達成するための最も簡単な方法は、模擬Deployer 実装の作成です。リスト4のこのテスト・ケースのコードによって、このテクニックがうまく示されています。

リスト4. 模擬Deployer実装
public class DeploymentTestCase extends TestCase {
     ... 
     public void testTargetFileDoesNotExist() {
         MockDeployer deployer = new MockDeployer(); 
         deployer.doNotFindAnyFiles(); 

         try {
             Deployment deployment = new Deployment(deployer); 
             deployment.deploy(new File("validLocation")); 
         }
         catch (FileNotFoundException success) {
         }
     }
     ...  
}

ここで、模擬Deployer に「どのようなファイル・オブジェクトが送られても、ファイルを探さないように」と伝えます。このテクニックを使用して、たとえばクライアントが、存在しないフォルダーへファイルを配置を試みるケースをシミュレートします。それをシミュレートするのではなく、なぜデタラメなファイル名を指定して実際に例外条件を作成しないのか不思議に思うかもしれません。例外条件のシミュレーションだけをしようというのであり、その実際の発生を試みているのではありません(後者は混乱を招きやすいので)。チームの新人開発者が、テスト・ケースの無効ファイルのロケーションとしてd:/doesNotExist を選択したとします。その開発者は自分のマシンでのテストに成功し、その変更を統合します。今度はあなたが、ファイルシステムにまさにd:/doesNotExistというファイルが作成されている自分のマシンでテストを行います。おそらくそれは正しく終了しなかったテスト・ケースに取り残されます。テストが予期せずに失敗するのは、コードが間違っているからではなく、テストがそれを取り巻く環境に過度に依存しているためです。

これでは時間の無駄です。問題の原因を分離するのに30分、d:/doesNotExist が危険な選択であった理由を新人開発者に説明するのに15分、チームの他のメンバー用にメモを作り、そのようなコーディングの実行に対する注意を呼びかけるために20分を費やしてしまいます。もちろん、新しい開発者がチームに加わった場合は、そのようなことがおそらくまた起こるでしょう。doNotFindAnyFiles と呼ばれるメソッドによって単一の模擬Deployerを記述すると、そのような煩わしさを回避できます。


シングルトンの統合 : Toolbox

シングルトンの使い過ぎは異なる角度からその問題を検討することによって回避できます。アプリケーションが1つのクラスの1つのインスタンスのみを必要とし、そのアプリケーションがスタートアップ時にそのようなクラスを構成するとします。なぜクラス自身がシングルトンであるべきことの責を負う必要があるのでしょうか?アプリケーションがその責を負う必要があるというのはきわめて論理的であると考えられます。なぜなら、アプリケーションこそがそのような振舞いを要求するからです。コンポーネントではなくアプリケーションがシングルトンとなるべきです。アプリケーションは、アプリケーション固有部分のコードで利用できるようにするために、そのようなコンポーネントのインスタンスを利用可能にします。アプリケーションはそのようなコンポーネントのいくつかを使用する際に、それらを、我々が呼ぶところのツールボックスに統合することができます。

簡単に言えば、アプリケーションのツールボックスは、その構成、またはアプリケーションのスタートアップ・メカニズムによる構成のいずれかを行うシングルトンです。Toolbox シングルトンの一般的なパターンを以下に示します。

リスト5. Toolboxシングルトンの一般的なパターン
public class MyApplicationToolbox {
     private static MyApplicationToolbox instance; 

     public static MyApplicationToolbox getInstance() {
         if (instance == null) {
             instance = new MyApplicationToolbox(); 
         }
         return instance; 
     }

     protected MyApplicationToolbox() {
         initialize(); 
     }

     protected void initialize() {
         // Your code here
     }

     private AnyComponent anyComponent; 

     public AnyComponent getAnyComponent() {
         return anyComponent(); 
     }
     ... 

     // Optional: standard extension allowing
     // runtime registration of global objects. 
     private Map components; 

     public Object getComponent(String componentName) {
         return components.get(componentName); 
     }

     public void registerComponent(String componentName, Object component) 
{
         components.put(componentName, component); 
     }

     public void deregisterComponent(String componentName) {
         components.remove(componentName); 
     }

}

Toolbox はそれ自身がシングルトンであり、さまざまなコンポーネント・インスタンスの一生を管理します。アプリケーションがそれを構成するか、またはアプリケーションにinitialize メソッド中で必要となる構成情報を記述するように要求します。これでアプリケーションはどのクラスのインスタンスがいくつ必要かを決定することができます。そのような決定における変更はアプリケーション固有のコードに影響を与える場合がありますが、再利用の可能なインフラストラクチャー・レベルのコードには影響しません。さらに、そのようなクラスはアプリケーションがそれらの使用を選択する方法に依存しないため、テスト・インフラストラクチャー・コードははるかに簡単なものになります。


本当にシングルトンである場合

クラスが本当にシングルトンであるか否かを決定するためのいくつかの質問があります。

  • すべてのアプリケーションは、まったく同一の方法でこのクラスを使用するか? (「まったく」がキーワード)
  • すべてのアプリケーションは、常にこのクラスの1つのインスタンスのみを必要とするか? (「常に 」と「1つの」がキーワード)
  • このクラスのクライアントは、自分自身がその一部に含まれているアプリケーションを意識しないべきか?

3つの質問に対する答えがすべて「イエス」であるなら、シングルトンです。ここでのキー・ポイントは、すべてのアプリケーションがクラスをまったく同一の方法で取り扱う場合、また、クライアントがアプリケーション・コンテキストなしでクラスを使用できる場合に、クラスがまさにシングルトンであるということです。

本当のシングルトンの典型例はログ・サービスです。イベントに基づくログ・サービスがあるとします。クライアント・オブジェクトはログ・サービスへのメッセージを送信し、テキストをログすることを要求します。実は他の(複数の)オブジェクトが、これらのログ要求を聞き、処理することにより、いずれか (コンソールやファイルなど)に収められているテキストをロギングします。まず、ログ・サービスはシングルトンになるための標準的なテストにパスすることを確認してみます。

  • リクエスターには、ログ要求を送信する宛先としてよく知られたオブジェクトが必要です。つまりこれがアクセスのグローバル・ポイントです。
  • ログ・サービスは、複数のリスナーが登録できる単一のイベント・ソースであるため、必要なのは1つのインスタンスであることのみです。

この標準的なシングルトン・デザイン・パターン要件は満たされていますが、その他の要件もあります。

  • 異なるアプリケーションは異なるアウトプット・デバイスにログを行うかもしれませんが、リスナーを登録する方法は常に同一です。カスタマイズはすべてリスナーによって行われます。クライアントは、テキストがログされる方法またはその場所を知らなくても、ログを要求することができます。したがって、すべてのアプリケーションはまったく同一の方法でログ・サービスを使用します。
  • どのアプリケーションも、ログ・サービスの1つのインスタンスのみで実行できます。
  • どのオブジェクトも、再利用可能なコンポーネントを含めて、ログ・リクエスターとなり得ます。その際、オブジェクトは特定のアプリケーションに結合されません。

標準的な要件の他に、ログ・サービスによって上述の要件も満たされています。これで、自分の選択を後悔することになるかもしれないという心配をせずに、ログ・サービスをシングルトンとして安全に実装できます。

これらのルールにもかかわらず、クラスをシングルトンにすべき場合には、コードを見てそこからそうすべきか考えるべきです。どう扱ったらよいか全然検討がつかないオブジェクトに出会った場合は、以下のような質問を行います。

  • このクラスのインスタンスをどこで獲得するか?
  • このオブジェクトは、記述中のアプリケーションに属するものか、あるいはコンポーネントに属するものか?
  • カスタマイズをこのクラスのクライアントに託せるようにそのクラスを記述できるか?

これがわかれば、それはおそらく本当にシングルトンでしょう。しかし、コードによって別の場所ではなくここでサブクラスを使用したいことが示されると、決定を再検討しなければなりません。

心配しないでください。コードはすべきことを必ず示してくれます。そのとおりにしてください。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。プロフィールで選択した情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=SOA and web services
ArticleID=243998
ArticleTitle=シングルトンの賢い使用法
publish-date=01072001