PG日誌

読者です 読者をやめる 読者になる 読者になる

PG日誌

主にc#の事を書いています

c#のstaticクラスとシングルトン考察


f:id:Takachan:20170115150341j:plain

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

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

staticクラス

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

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

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

public static class GlobalDataByStaticClass
{
    // インスタンスメンバーを宣言するとコンパイルエラーになる
    public string Name { get; set; }
}
public void ClientMethod()
{
    // 以下のように新しいインスタンスの宣言はできない
    GlobalDataByStaticClass global = new GlobalDataByStaticClass();
}

また、staticクラスはsealed宣言したクラスと同様に派生クラスを持てません。インターフェースを継承する事や、抽象クラス宣言も行えません。

UMLで表記するとこんな感じになります。

f:id:Takachan:20160104200022p:plain

UMLにはstaticクラスの概念が無いため、クラスに<>ステレオタイプを付与しています。また、全てのメンバーがstaticになるので全てのメンバーはstaticを表す下線を引いています。

上記クラス図のc#の実装は以下の通りです。

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

    public static string Name { get; set; }
}

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

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

f:id:Takachan:20160104202254p:plain

Singletonクラス

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

f:id:Takachan:20160104200551p:plain

もしくは

f:id:Takachan:20160104201042p:plain

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

というわけで、下のUMLc#の実装は以下の通り。

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

    // インスタンスの取得
    public static GlobalDataBySingleton
    {
        get
        {
            if(GlobalDataBySingleton.instance == null)
            {
                GlobalDataBySingleton.instance =
                    new GlobalDataBySingleton();
            }
            return GlobalDataBySingleton.instance;
        }
    }

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

    // 各メンバーの宣言

    public uint ID { get; set; }

    public string Name { get; set; }
}

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

どう違うのか?

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

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

経験上、staticクラスを安易に用いた場合、各メンバーはglobal変数と同じため、これに値を外部から設定できるようにしているとかなり痛い目に合います。特に複雑化したシステムで当初存在しなかったコードからの値の設定がされた場合大惨事の原因になるのでsetterは必ずprivateにしておきましょう。

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;
}

Utilityクラスも同じように使えますね。

テスト視点で見てみると...

但し、上記特性は特に自動テストを通す局面で違いが大きく現れます。

staticクラスの場合、クラス内でメンバーの初期化を行うとテスト時、本番時の区別が出来ない(値を状況によって変更できない)ため外部から値を設定している場合割と複雑な事になります。(但し、以下例は理想なので何もここまでしなくていいということもありますが…)

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

f:id:Takachan:20160104204034p:plain

この構成をstaticクラスを作るたびに作っているとかなり手間です。

Sinsletonは書関『レガシーコード改善ガイド』のP138によれば

f:id:Takachan:20160104210606p:plain

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

まとめ

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

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

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

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

参考書籍

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

レガシーコード改善ガイド (Object Oriented SELECTION)

レガシーコード改善ガイド (Object Oriented SELECTION)

新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES)

新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES)