オタクタイム-デザインパターンの美しさシングルトンモード(中央):シングルトンモードの使用をお勧めしないのはなぜですか?選択肢は何ですか?

シングルトンは非常に一般的に使用されるデザインパターンですが、実際の開発では頻繁に使用されます。ただし、シングルトンはアンチパターンであると考える人もいるため、お勧めしません。それで、今日、私はこの声明に応えてこれらの問題について詳細に話します:シングルトンデザインパターンの問題は何ですか?なぜアンチパターンと呼ばれるのですか?シングルトンを使用しない場合、グローバルに一意のクラスを表すにはどうすればよいですか?代替ソリューションは何ですか?

シングルトンの問題は何ですか?

ほとんどの場合、プロジェクトではシングルトンを使用します。シングルトンは、構成情報クラス、接続プールクラス、IDジェネレータークラスなど、グローバルに一意のクラスを表すために使用されます。シングルトンモードは、記述が簡単で使いやすいです。コードでは、オブジェクトを作成する必要はなく、IdGenerator.getInstance()。getId()などのメソッドを介して直接呼び出すだけです。ただし、この使用方法はハードコードに少し似ているため、多くの問題が発生します。次に、特定の問題を見てみましょう。

1.シングルトンはOOP機能に対応していません

OOPの4つの主要な特性は、カプセル化、抽象化、継承、および多態性であることを私たちは知っています。シングルトンデザインパターンは、抽象化、継承、および多態性をサポートしていません。なんでそんなこと言うの?説明には、IdGeneratorの例を引き続き使用します。


public class Order {
    
    
  public void create(...) {
    
    
    //...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

public class User {
    
    
  public void create(...) {
    
    
    // ...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

IdGeneratorの使用は、実装ではなくインターフェイスに基づく設計原則に違反し、広義に理解されているOOPの抽象的な特性にも違反します。将来的には、ビジネスごとに異なるID生成アルゴリズムを採用したいと考えています。たとえば、注文IDとユーザーIDは、異なるIDジェネレーターを使用して生成されます。この需要の変化に対処するには、IdGeneratorクラスが使用されるすべての場所を変更して、コードの変更が比較的大きくなるようにする必要があります。


public class Order {
    
    
  public void create(...) {
    
    
    //...
    long id = IdGenerator.getInstance().getId();
    // 需要将上面一行代码,替换为下面一行代码
    long id = OrderIdGenerator.getIntance().getId();
    //...
  }
}

public class User {
    
    
  public void create(...) {
    
    
    // ...
    long id = IdGenerator.getInstance().getId();
    // 需要将上面一行代码,替换为下面一行代码
    long id = UserIdGenerator.getIntance().getId();
  }
}

さらに、シングルトンは継承や多形性に適していません。ここで「完全にサポートされていない」ではなく「非友好的」という言葉を使用する理由は、理論的にはシングルトンクラスも継承でき、多態性を実現できるためですが、実装は非常に奇妙です。これにより、コードの読みやすさが低下します。デザインの意図を理解していない人は、そのようなデザインを説明できないことに気付くでしょう。したがって、クラスをシングルトンクラスに設計することを選択すると、継承と多態性という2つの強力なオブジェクト指向機能を放棄することを意味します。これは、要件の将来の変更に対応できるスケーラビリティを失うことに相当します。 。

2.シングルトンはクラス間の依存関係を隠します

コードの読みやすさが非常に重要であることを私たちは知っています。コードを読むとき、クラス間の依存関係を一目で確認し、このクラスがどの外部クラスに依存しているかを把握したいと考えています。

コンストラクターやパラメーターの受け渡しなどで宣言されたクラス間の依存関係は、関数の定義を見れば簡単に識別できます。ただし、シングルトンクラスを明示的に作成する必要はなく、パラメータの受け渡しに依存する必要もありません。また、関数で直接呼び出すこともできます。コードがより複雑な場合、この呼び出し関係は非常に隠されます。コードを読むときは、各関数のコード実装を注意深くチェックして、このクラスがどのシングルトンクラスに依存しているかを知る必要があります。

3.シングルトンはコードのスケーラビリティに適していません

シングルトンクラスは1つのオブジェクトインスタンスしか持てないことがわかっています。将来、コード内に2つ以上のインスタンスを作成する必要がある場合は、コードに比較的大きな変更を加える必要があります。あなたは言うかもしれません、そのような需要はありますか?ほとんどの場合、シングルトンクラスはグローバルクラスを表すために使用されるので、なぜ2つ以上のインスタンスが必要になるのでしょうか。

実際、そのような需要は珍しいことではありません。説明する例として、データベース接続プールを取り上げましょう。

システム設計の初期段階では、データベース接続リソースの消費を制御できるように、システムには1つのデータベース接続プールのみが必要であると考えていました。したがって、データベース接続プールクラスをシングルトンクラスとして設計しました。しかし、その後、システム内の一部のSQLステートメントの実行が非常に遅いことがわかりました。これらのSQLステートメントが実行されると、データベース接続リソースを長時間占有し、他のSQL要求が応答しなくなります。この問題を解決するために、実行のために遅いSQLを他のSQLから分離したいと考えています。この目標を達成するために、システムに2つのデータベース接続プールを作成できます。遅いSQLには排他的なデータベース接続プールがあり、他のSQLには排他的なデータベース接続プールがあります。これにより、遅いSQLが他のSQLの実行に影響を与えるのを防ぐことができます。

データベース接続プールをシングルトンクラスとして設計すると、明らかにそのような要件の変更に適応できなくなります。つまり、シングルトンクラスがコードのスケーラビリティと柔軟性に影響を与える場合があります。したがって、データベース接続プールやスレッドプールなどのリソースプールをシングルトンとして設計しないことをお勧めします。実際、一部のオープンソースデータベース接続プールとスレッドプールは、実際にはシングルトンとして設計されていません。

4.シングルトンはコードのテスト容易性に適していません

シングルトンモードを使用すると、コードのテスト容易性に影響します。シングルトンクラスがDBなどの比較的重い外部リソースに依存している場合、ユニットテストを作成するときに、モックで置き換えることを望んでいます。シングルトンクラスのハードコードされた使用は、模擬置換を実装することを不可能にします。

さらに、シングルトンクラスがメンバー変数(IdGeneratorのidメンバー変数など)を保持している場合、実際には、すべてのコードで共有される一種のグローバル変数と同等です。このグローバル変数が可変グローバル変数である場合、つまりそのメンバー変数を変更できる場合、ユニットテストを作成するときは、さまざまなテストケースにも注意を払い、シングルトンクラスを変更する必要があります。同じメンバー変数の値は、テスト結果の相互影響の問題を引き起こします。

5.シングルトンはパラメータ化されたコンストラクタをサポートしていません

シングルトンはパラメータ化されたコンストラクタをサポートしていません。たとえば、接続プールのシングルトンオブジェクトを作成する場合、パラメータを使用して接続プールのサイズを指定することはできません。この問題に対応して、どのような解決策が利用できるか見てみましょう。

最初の解決策は、インスタンスを作成した後、init()関数を呼び出してパラメーターを渡すことです。このシングルトンクラスを使用する場合、getInstance()メソッドを呼び出す前にinit()メソッドを呼び出す必要があることに注意してください。そうしないと、コードが例外をスローします。具体的なコードの実装は次のとおりです。


public class Singleton {
    
    
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton(int paramA, int paramB) {
    
    
    this.paramA = paramA;
    this.paramB = paramB;
  }

  public static Singleton getInstance() {
    
    
    if (instance == null) {
    
    
       throw new RuntimeException("Run init() first.");
    }
    return instance;
  }

  public synchronized static Singleton init(int paramA, int paramB) {
    
    
    if (instance != null){
    
    
       throw new RuntimeException("Singleton has been created!");
    }
    instance = new Singleton(paramA, paramB);
    return instance;
  }
}

Singleton.init(10, 50); // 先init,再使用
Singleton singleton = Singleton.getInstance();

2番目の解決策は、getIntance()メソッドにパラメーターを配置することです。具体的なコードの実装は次のとおりです。


public class Singleton {
    
    
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton(int paramA, int paramB) {
    
    
    this.paramA = paramA;
    this.paramB = paramB;
  }

  public synchronized static Singleton getInstance(int paramA, int paramB) {
    
    
    if (instance == null) {
    
    
      instance = new Singleton(paramA, paramB);
    }
    return instance;
  }
}

Singleton singleton = Singleton.getInstance(10, 50);

上記のコードの実装に少し問題があることに気づいたかどうかはわかりません。getInstance()メソッドを次のように2回実行すると、取得したsingleton1とsignleton2のparamAとparamBは両方とも10と50になります。言い換えると、2番目のパラメーター(20、30)は機能せず、構築プロセスはプロンプトを表示せず、ユーザーを誤解させる可能性があります。この問題を解決する方法は?あなた自身の考えに任せてください、あなたはメッセージエリアであなたの解決策のアイデアについて話すことができます。


Singleton singleton1 = Singleton.getInstance(10, 50);
Singleton singleton2 = Singleton.getInstance(20, 30);

3番目の解決策は、パラメーターを別のグローバル変数に配置することです。具体的なコードの実装は次のとおりです。Configは、paramAとparamBの値を格納するグローバル変数です。内部の値は、次のコードのような静的定数で定義することも、構成ファイルからロードすることもできます。実際、この方法が最も推奨されます。


public class Config {
    
    
  public static final int PARAM_A = 123;
  public static final int PARAM_B = 245;
}

public class Singleton {
    
    
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton() {
    
    
    this.paramA = Config.PARAM_A;
    this.paramB = Config.PARAM_B;
  }

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

代替ソリューションは何ですか?

シングルトンに関する多くの問題について触れました。シングルトンに関する問題がそれほど多くても、私はそうする必要はないと言うかもしれません。私のビジネスにはグローバルに一意のクラスが必要です。シングルトンを使用しない場合、このクラスのオブジェクトがグローバルに一意であることを確認するにはどうすればよいですか?

グローバルな一意性を確保するために、シングルトンを使用することに加えて、静的な方法を使用して実現することもできます。これは、プロジェクト開発でよく使用される実装のアイデアでもあります。たとえば、前のレッスンで説明した一意のID増分ジェネレータの例は、次のような静的メソッドを使用して実装できます。


// 静态方法实现方式
public class IdGenerator {
    
    
  private static AtomicLong id = new AtomicLong(0);
  
  public static long getId() {
    
     
    return id.incrementAndGet();
  }
}
// 使用举例
long id = IdGenerator.getId();

ただし、静的メソッドを実現しても、前述の問題は解決されません。実際、シングルトンよりも柔軟性がなく、たとえば、レイジーロードをサポートできません。他に方法があるかどうか見てみましょう。実際、以前使用していた方法に加えて、シングルトンを使用する別の方法があります。具体的なコードは次のとおりです。


// 1. 老的使用方式
public demofunction() {
    
    
  //...
  long id = IdGenerator.getInstance().getId();
  //...
}

// 2. 新的使用方式:依赖注入
public demofunction(IdGenerator idGenerator) {
    
    
  long id = idGenerator.getId();
}
// 外部调用demofunction()的时候,传入idGenerator
IdGenerator idGenerator = IdGenerator.getInsance();
demofunction(idGenerator);

新しい使用方法に基づいて、シングルトンによって生成されたオブジェクトをパラメーターとして関数に渡します(コンストラクターを介してクラスのメンバー変数に渡すこともできます)。これにより、シングルトンクラス間の隠れた依存関係の問題を解決できます。ただし、不適切なOOP機能、スケーラビリティ、テスト容易性など、シングルトンに関するその他の問題は解決できません。

したがって、これらの問題を完全に解決したい場合は、ルートからグローバルに一意のクラスを実装する他の方法を見つける必要があるかもしれません。実際、クラスオブジェクトのグローバルな一意性は、さまざまな方法で保証できます。シングルトンモデル、またはファクトリモデルのIOCコンテナ(Spring IOCコンテナなど)を介して保証を適用できますが、プログラマ自身も保証を適用できます(コードを作成するときに2つのクラスを作成しないようにしてください)。オブジェクト)。これは、Javaでメモリオブジェクトのリリースを担当するJVMに似ていますが、C ++ではプログラマが担当します。真実は同じです。

代替ファクトリモデルとIOCコンテナの詳細な説明については、次の章で説明します。

おすすめ

転載: blog.csdn.net/zhujiangtaotaise/article/details/110443138