データ共有目的でシングルトンパターンを使ってはいけない理由
シングルトンは、実質的にグローバル変数のように振る舞います。
どこからでもアクセスできるため、予期せぬ場所から変更されてしまうケースが非常に多いです。
悪いサンプルコード
public class SharedData {
private static SharedData instance;
private String data;
private SharedData() {
}
public static SharedData getInstance() {
if (instance == null) {
instance = new SharedData();
}
return instance;
}
// セッター・ゲッターによってどこからでもアクセス可能
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
上記のサンプルコードでは、複数のクラスがsetDate()メソッドを呼び出せてオブジェクトの状態を変更することができてしまいます。
これにより、予期せぬデータ変更により不具合が起きてしまう可能性が高まります。
シングルトンパターンに限らず、複数のクラスからアクセスできてしまうデータ共有の仕組みはこのような問題を抱えています。
僕が最近出会ったシングルトンパターンの間違った使い方を紹介します。
readHddFile();
String maillAddress = ConfigFile.getInstance().getMailAddress();
public void readHddFile() {
// HDDのファイルを読み取る処理(ここでは省略)
// シングルトンインスタンスを戻り値のような役割で使用している
ConfigFile.getInstance().setMailAddress(maillAddress);
}
このコードを見た時、正直殺意が湧いたレベルです。
readHddFile()メソッドの定義を見ると戻り値はvoidなのに、メソッド内部でシングルトンオブジェクトの状態を変更して、呼び出し元で値の参照をしてします。
これは、メソッド同士が密結合になってしまい変更に弱くなってしまいます。
また、readHddFile()メソッドの詳細を確認しなければならないため、可読性の面でも最悪です。
このように、シングルトンパターンをデータ共有目的で使用してしまうと、簡単に密結合の状態を作り出せてしまい、変更に弱い設計が出来上がってしまいます。
シングルトンパターンには状態を持たない共通機能の提供のみとすること
シングルトンパターンの状態は、絶対に変更されるべきではありません。
代表的な例として、以下のような用途で使用するべきです。
- ログ出力クラス(Logger)
- 設定ファイルの読み取り(ConfigLoader)
- データベース接続管理(ConnectionPool)
- キャッシュ管理クラス(CacheManager)
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;
public class ConnectionPool {
private static final int POOL_SIZE = 5;
private static ConnectionPool instance;
private LinkedList<Connection> pool = new LinkedList<>();
private final String URL = "jdbc:mysql://localhost:3306/mydb";
private final String USER = "root";
private final String PASSWORD = "password";
private ConnectionPool() {
try {
for (int i = 0; i < POOL_SIZE; i++) {
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
pool.add(conn);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* データベース接続用の情報は頻繁に変更されるものではない
* そのため、1度だけオブジェクトを生成して他クラスから参照できるようにすることで再利用ができる。
* シングルトンパターンでは、状態を変更できるようなセッターメソッドを追加しないことが重要
**/
public static synchronized ConnectionPool getInstance() {
if (instance == null) {
instance = new ConnectionPool();
}
return instance;
}
public synchronized Connection getConnection() {
if (pool.isEmpty()) {
throw new RuntimeException("すべてのコネクションが使用中です。");
}
return pool.removeFirst();
}
public synchronized void releaseConnection(Connection conn) {
pool.addLast(conn);
}
}
データの共有はせずに、データを受け渡しすること
シングルトンパターンに限らず、複数クラス間でデータを共有できる仕組みは密結合の状態が生まれやすいです。
そのため、引数や戻り値を駆使してデータを受け渡しするようにしましょう。
この仕組みを考えるのは大変ですが、引数で受け取った値しか使わないようにすることで、疎結合の状態になり変更が容易な設計にすることができます
データ共有の仕組みを作るのは簡単ですが、それに依存してしまうとあっという間に変更が困難なコードの出来上がりです。
シングルトンパターンを設計する際は、本当に状態変更されることのないオブジェクトになるか 検討したうえで採用しましょう。