【C#】staticクラスとシングルトン考察

C# には言語固有機能として static クラスという機能があります。クラス自体を static と宣言することで、インスタンス作成を禁止し、static 宣言したクラスのインスタンスが複数作成できないようにできます。ですがこの機能、デザインパターンにあるシングルトンと何か違うのでしょうか?ちょっと考察してみました。

まずは、static クラスと Singleton 各々が C# 言語上でどういう風に表現されるか見ていきたいと思います。

staticクラスの宣言

static クラスの宣言方法は簡単で、クラスの前に static を付けます。

// 宣言に「static」を追加
public static class GlobalDataByStaticClass ...

こうすることによってこのクラスの新規作成、インスタンスメンバーの保持を禁止することができます。

public static class GlobalDataByStaticClass
{
    // こういった非staticなメンバーを宣言するとコンパイルエラーになる
    public string Name { get; set; }
}
public void ClientMethod()
{
    // 以下のようにnewでオブジェクトは作成できない = コンパイルエラー
    GlobalDataByStaticClass global = new GlobalDataByStaticClass();
}

また、staticクラスは継承元として使えず、ほかのクラスが継承することもできません。

ちなみに、UMLでstaticクラス表記するとこんな感じになります。(全部staticメンバーになるので下線が全てにつきます)

staticクラスの概念は<>ステレオタイプで表現します。また、上記クラス図のC#の実装は以下の通ようになります。

public static GlobalDataByStaticClass
{
    public static uint ID { get; set; }

    public static string Name { get; set; }
}

但し、このままでは値が設定されないので値を設定するためのメソッドを自分自身に用意するか、他人に設定してもらう必要があります。

以下の例ではアプリケーションの横断的な関心を管理するためのクラス「ApplicationCoordinator」を追加してGlobalDataByStaticClassの各メンバーの値を設定しています。


Singletonクラス

GoFのデザインパターンで出てくるシングルトンパターンですが、C#で記述すると以下の様になります。

もしくは

となるかと思います。
プロパティを用いてインスタンスを取得する下の表記のほうがよりc#っぽいです。

というわけで、後述の場合のシングルトンのC#の実装は以下の通りです。

public class Singleton
{
    // 唯一のインスタンス
    private static GlobalDataBySingleton instance;

    // インスタンスの取得
    public static Singleton{ get; } = new Singleton();

    // コンストラクタをアクセス禁止にして
    // 外部からのインスタンスの作成を禁止する
    private GlobalDataBySingleton() { }

    // 各メンバーの宣言
    public uint ID { get; set; }
    public string Name { get; set; }
}

この例では、初回アクセス時にインスタンスが無ければ作成しています。

どう違うのか?

staticクラスとSingletonでは以下の違いがあります。

項目 staticクラス Singleton
継承 できない できる
仮想メンバー 持てない 持てる
抽象メンバー 持てない 持てる
インターフェース 持てない 持てる
コード量 少ない staticクラスに比べると多い

上記の特性を考慮し、データを読み書きする場合、プロセス中で必ず唯一のデータを扱える。という意味で利用感は全く同じですが、シングルトンは派生クラスによってメソッドをオーバーライドできるので、テストのときにダミーデータ応答をするテストオブジェクトに差し替えるなど柔軟な対応が可能です。

staticクラスの場合、ほぼ一瞬で実装が完了するので大変軽いのですが、C言語時代にあったグローバル変数と同じで、派生クラスによる差し替えもできないため実装がシングルトンに比べ固くなります。

局所的なデータ共有や、すぐさま実装する場合はstaticクラス、可用性や実装の柔らかさを確保したいときはシングルトンという利用イメージでほぼ間違いないと思います。

もしstaticクラスでデータ共有する場合は、フィールド生公開ではなく必ずプロパティでくるむかアクセサ経由にしましょう。

Javaで言うところの乗数クラスで使う場合

よくJavaでこんな感じの定義をしますが

public class Define
{
    // インスタンス作成禁止
    private Define() { }

    public static final int I = 1;
    public static final int P = 2;
    public static final int H = 4;
}

c#のstaticクラスはそのままより簡単に書けます

public static class Define
{
    public const int I = 1;
    public const int P = 2;
    public const int H = 4;
}

staticクラスの欠点

で、実際に運用を始めたときに特に違いがでるのが、自動テストを通す局面だったりします。

staticクラスの場合、クラス内でメンバーの初期化を行うとテスト時、本番時の区別が出来ない(値を状況によって変更できない)ため外部からデバッグの値の設定と本番時の値を設定する側が書き分ける必要が出てきます。

以下例ではApplicationCoordinatorの値のロード操作をもつインターフェースクラスIApplicationCoordinatorをあたらに追加してテスト用データをロード設定するためのTestContext、実行状況によりどちらのインスタンスを使うのかを定めるFactoryを追加し利用します。

この構成をstaticクラスを作るたびに作っているとかなり手間です。Sinsletonは書関『レガシーコード改善ガイド』のP138によれば

コンストラクタをprotectedへ変更してstaticなインスタンス変更メソッドを追加し柔軟性を出すことができます。この場合、テスト時は初期化の段階でTextContextを作成し、任意の値を設定した後にSetInstansへテストデータを与え処理を実行します。

まとめ

長くなりましたがまとめると

テストコードに含めない比較的シンプルな実装や、使い捨てのオブジェクトの場合staticクラス(Loadメソッドをstaticクラス自身に持たせれば依存関係もシンプルに保てます)

テストで取り扱い将来的に複雑化しそう、複数人での開発による役割分担がある場合Singleton(特に後からメンバーの挙動を追加して再配布すると大惨事になる場合あらかじめSetInstanceできる形式で配布すると後で幸せかと思います)

とするのがベターな選択かと思います。