なぜstaticメソッドではなくシングルトンで実装するのか


疑問

自分でシングルトンパターンでクラスを定義しているときに、ふと疑問に思ったことがあったので調べて見ました。
その疑問とは

なぜクラスメソッドではなくシングルトンで実装するのか

ということです。
これを深めていくと、自分はそもそもなぜシングルトンパターンを使ってクラスを実装していたのかが理解できていませんでした。

追記:
20210213にシングルトンを使用するメリットを追加いたしました。

どういう意味か

自分のプライベートプロジェクトで、プロパティを管理するためのクラスが欲しいと思い、私は以下のような実装を行いました。

PropertyService.java
import java.util.HashMap;
import java.util.Map;

public class PropertyService {

    private static final PropertyService INSTANCE = new PropertyService();
    private Map<String, String> props = new HashMap<String, String>();

    private PropertyService(){
        initializeProps();
    }

    public static PropertyService getInstance() {
        return INSTANCE;
    }

    private void initializeProps() {
        // プロパティを初期化するメソッドを実装する
        // 便宜的に手動で値を挿入する。
        props.put("a", "田中");
        props.put("b", "鈴木");
        props.put("c", "佐藤");
    }

    public String getVal(String key) {
        return this.props.get(key);
    }

}

上記の実装はシングルトンパターンになっています。
私としては「プロジェクトに共通となるプロパティを管理できるようにしなければならないわけだから、インスタンスが必ず一つになるよう実装される「シングルトンパターン」を実装に使うべきだな」と思ったわけです。
しかし、上記のクラス、クラスメソッドを用いて以下のようにも実装できるわけです。

PropertyUtils.java
import java.util.HashMap;

public class PropertyUtils {

    private static final HashMap<String, String> props = new HashMap<String, String>();

    static {
        // プロパティ初期化処理
        props.put("a", "田中");
        props.put("b", "鈴木");
        props.put("c", "佐藤");
    }

    public static String getVal(String key) {
        return props.get(key);
    }

}

いわゆるユーティリティクラスですね。
こちらの方が実装が簡単な上に、プロパティの一元管理も実現できそうです。

では、なぜシングルトンパターンでの実装が必要なのでしょうか?
どうして、クラスメソッドでの実装ではなく、「常にたった一つのインスタンス」が必要になるのでしょうか?

シングルトンパターンを使う必要がある場合

インターフェースを実装したい場合

例えばインターフェースを実装したい場合はstaticメソッドでは代用することができません。

TestInterface.java
public interface TestInterface {

    public void sayHello(String name);

}
TestImplements.java
public class TestImplements implements TestInterface {

    private static final TestImplements INSTANCE = new TestImplements();

    private TestImplements() {}

    public static TestImplements getInstance() {
        return INSTANCE;
    }

    @Override
    public void sayHello(String name) {
        System.out.println("Hello " + name + " !");
    }

}

もちろんインターフェースでstaticメソッドを定義し、実装クラスで同じ名前のstaticメソッドを定義することもできるのですが、それはオーバーライドではなく、全く別のメソッドという扱いになってしまうので、わざわざインターフェースを実装するメリットがなくなってしまうのです。
むしろ、インターフェースでstaticメソッドを定義してしまうと、実装クラスを作成せずともそのメソッドは使えてしまうので、上記のようにインターフェースを実装したい場合は、やはりシングルトンパターンを使わざるを得ません。

インターフェースでstaticメソッドを実装した場合
public interface TestInterface {

    public static void sayHello(String name) { 
        System.out.println("Hello " + name + " !");
    }

}
staticメソッドは実装せずとも使えてしまう
public class TestImplements implements TestInterface {

    ...(中略)...

    public static void sayHello(String name) {
        TestInterface.sayHello(name);
    }

}

20210213追記:
シングルトンを使うメリットは、この「インターフェースを実装したい場合」が最大であると思われます。

それは、outがPrintStreamインターフェースを実装しているからです。PrintStreamインターフェースを軸に、プログラムを変更することなく出力先を自由に差し替えられる仕組みとして提供されているからです。一般化して、シングルトンパターンを使う理由はこう説明しないといけないのです。 - シングルトンパターンの意義

例えばDBへの接続を担うConnectorクラスは、アプリケーション全体で共通のものが使用されることが多いです。しかし、接続するDB自体は選択可能としたい。そういった場合はインターフェースを実装する必要があり、このとき、Connectorクラスはシングルトンとして実装される必要があると思われます。

抽象クラスの継承クラスとして実装したい場合

抽象クラスのメソッドをOverrideする場合はstatic修飾子を付与することはできませんので、シングルトンパターンとして実装する必要があります。

TestAbstract.java
abstract public class TestAbstract {

    abstract public void sayHello(String name);

}
TestExtends.java
public class TestExtends extends TestAbstract {

    private static final TestExtends INSTANCE = new TestExtends();

    private TestExtends() {}

    public static TestExtends getInstance() {
        return INSTANCE;
    }

    @Override
    public void sayHello(String name) {
        System.out.println("Hello" + name + " !");
    }

}

DIに設定したい場合

例えばspringなどでbean定義したい場合は必ずクラスのインスタンスをbeanとして設定しなければなりません。
なのでアプリの共通のクラスにしたい、かつそのクラスをDIとして設定したい場合はシングルトンパターンで実装する必要があります。

SpringのBeanに設定したい場合
@Configuration
public class BeanConfiguration {

    @Bean
    public TestInterface testInterface() {
        return TestImplements.getInstance();
    }

}

依存性を注入したい場合

例えばspringなどのAutowiredで依存性を注入したい場合は、注入するフィールドをstaticにはできませんので、シングルトンパターンで実装する必要があります。

public class TestImplements implements TestInterface {

    ...(中略)...

    @Autowired
    TestService testService;

    ...(中略)...

}

しかし、staticフィールドに依存性を注入する方法があるにはありますが、やや強引な実装になるようです(参考に記載のサイトをご参照ください)。

一応staticフィールドに依存性を注入することはできる
public class TestImplements implements TestInterface {

    ...(中略)...

    static TestService testService;

    @Autowired
    public void setTestService(TestService service) {
        testService = service;
    }

    ...(中略)...

}

ですが、結局setterは非staticの必要があるので、シングルトンパターンで実装しなければなりません。

シングルトンを使わなくてよいとき

状態を保持しないとき

例えば、以下のクラスのように「常に決まった値しか返さないメソッドしか実装していないようなクラス」の場合は、わざわざシングルトンパターンで実装する理由はありません。

TestSingleton.java
public class TestSingleton {

    private static final TestSingleton INSTANCE = new TestSingleton();

    private TestSingleton() {}

    public static TestSingleton getInstance() {
        return INSTANCE;
    }

    public void sayHello() {
        System.out.println("HelloWorld");
    }

}

上記のようなクラスの場合は、冒頭で述べたようにユーティリティクラスとして実装すればよいわけです。

まとめ

今回は、なぜシングルトンの「一つしかインスタンスを作ることができない」というデザインがアプリケーションを作成する上で利点になるのか疑問になったので勉強をしてみました。
なぜそんなことを考えたかというと、上記でも述べたように、一部のシングルトンはstaticメソッドで代用することが可能だからです。
私は今回の学習でシングルトンパターンを使いたいときは、それがただ一つの「インスタンス」だからこその利点を見出さなければならないと思いました。

一旦現状私の考えつく範囲でシングルトンパターンで実装するべき場合とそうでない場合を羅列しましたが、これからも何か気づいたことがありましたら随時追記していくつもりです。

参考:
Singleton パターンの使いどころをまとめてみた
Pros and Cons of Singleton.
Javaのstaticメソッドを丁寧に解説! 活用例や考え方も一緒に学ぼう!
staticフィールドを@Autowiredの対象にしたい