T.TAO
Back to Blog
/14 min read/Programming

Unity Engine #3 Design Pattern 1

#programming
Unity Engine #3 Design Pattern 1

Unity Engine #3 デザインパターン 1

本記事は、各種コードのデザインパターンを主に紹介します。GoF の著書「デザインパターン:オブジェクト指向ソフトウェアの再利用のための基盤」では、合計 23 のデザインパターンが紹介されています。その中で、Unity で非常に広く重く使用されているパターンは、主にこのノートのシングルトンパターン、オブザーバーパターン、ファクトリーパターンです。

シングルトンパターン

シングルトン(Singleton) とは、クラスがシーン内で1つのインスタンスのみを持つ場合、グローバルなアクセスポイントを提供できることを意味します。一般的な使用例は各種 Manager です。以下は GameManager シングルトンの従来のシングルトン書き方です:

Plain Textpublic class GameManager : MonoBehaviour
{
    // private singleton
    private static GameManager _instance;
    
    // public singleton for others to get;
    public static GameManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindObjectOfType<GameManager>();
                if (_instance == null)
                {
                    _instance = new GameObject("Spawned GameManager", typeof(GameManager)).GetComponent<GameManager>();
                }
            }
            return _instance;
        }
    }

    private void Awake()
    {
        // since we are using the Singleton pattern, we should always assume that we only have the current instance in this scene
        if (_instance != null && _instance != this)
        {
            // if another instance is found, we destroy it;
            Destroy(this.gameObject);
        }
        else
        {
            _instance = this;
            DontDestroyOnLoad(this.gameObject);
        }
    }
    
    public void SomeFunction() {
        // ... some implementation ...
        return;
    }
}

その後、他の C# スクリプトで

Plain TextGameManager.Instance.SomeFunction();

を使用して、GameManager の public 関数と変数を呼び出せます。SerializedField を個別に設定したり、現在のシーンの GameManager インスタンスを検索する必要はありません。

シングルトンパターンの利点

  1. インスタンス数の制御:シングルトンパターンは、アプリケーションのライフサイクル中にクラスが1つのインスタンスのみを持つことを保証する。
  2. グローバルアクセスポイント:シングルトンインスタンスはグローバルにアクセスでき、アプリケーション内のどこからでもアクセスでき、オブジェクトを繰り返し作成したりパラメータを通じて渡す必要がない。
  3. 状態の保存:シングルトンはアプリケーションのライフサイクル全体で状態を維持できる。設定データ、キャッシュ、ログなど、複数のシステムやコンポーネントで共有されるデータの格納に有用。
  4. リソース消費の削減:大量のオブジェクトを作成(または破棄)する必要がなくなるため、GC 呼び出しを減らせる。
  5. 統一されたリソース管理:データベース接続やファイルシステムアクセスなど、複数の場所で使用する共有リソースがある場合、シングルトンはこれらのリソースを管理する統一されたポイントを提供し、リソース使用の同期と一貫性を確保できる。
  6. 遅延初期化:シングルトンは**遅延初期化(Lazy Initialization)**を実装でき、実際に必要になるまでオブジェクトを作成しない。アプリケーションの起動時間とリソース消費の削減に役立つ。

シングルトンパターンの問題点

シングルトンパターンには多くの利点がありますが、潜在的な問題にも注意が必要です。例えば、グローバル状態の問題を導入し、コードのテストと保守を困難にする可能性があります。また、誤って使用すると、特にマルチスレッド環境では、シングルトンはリソースの同期問題を引き起こす可能性があります。

シングルトンパターンを使用する際は、その利便性と潜在的なリスクを秤にかけ、適切なシナリオで慎重に使用する必要があります。

オブザーバーパターン

オブザーバーパターン(Observer Pattern) は、オブジェクト間の一対多の依存関係を定義します。オブジェクト/被観察者(Subject) の状態が変化すると、オブザーバー(Observer) がすべて通知され、自動的に更新されます。

被観察者が変化すると、他のオブジェクトを変更する必要がある。同時に、被観察者何人のオブザーバーが自分を観察しているか知らない。このような場合にオブザーバーパターンを使用する。

以下はオブザーバーパターンの従来の書き方です。

Plain Text// オブザーバーインターフェース
public interface IObserver
{
    void Update();
}

// 被観察者インターフェース
public interface ISubject
{
    // 新しい Observer を登録
    void RegisterObserver(IObserver observer);
    
    // 既存の Observer を削除
    void RemoveObserver(IObserver observer);
    
    // 現在のすべての Observer に通知
    void NotifyObservers();
}

// 具体的主体
public class ConcreteSubject : ISubject
{
    private List<IObserver> observers = new List<IObserver>();
    private float someState;

    public void RegisterObserver(IObserver observer)
    {
        observers.Add(observer);
    }

    public void RemoveObserver(IObserver observer)
    {
        observers.Remove(observer);
    }

    public void NotifyObservers()
    {
        foreach (var observer in observers)
        {
            observer.Update();
        }
    }

    public void ChangeState(float state)
    {
        someState = state;
        NotifyObservers();
    }
}

// 具体的オブザーバー
public class ConcreteObserver : IObserver
{
    private ConcreteSubject subject;
    private float observerState;

    public ConcreteObserver(ConcreteSubject subject)
    {
        this.subject = subject;
        this.subject.RegisterObserver(this);
    }

    // 更新操作
    public void Update()
    {
        observerState = subject.SomeState; 
    }
}

ここでは、interface / struct / class の違いを比較します。

インターフェース(Interface)

  • 定義:インターフェースは、一連のメソッドとプロパティを定義するが実装を提供しない仕様または契約。メンバーを宣言するだけで、メンバーの実装は含まない。
  • 目的:オブジェクトが持つべきメソッドとプロパティを定義するが、これらのメソッドを実装しない。無関係なクラスが同じインターフェースを実装し、同じように扱われることを可能にする。
  • 特徴:データフィールドを含めない。メソッド、プロパティ、インデクサー、イベントの宣言を含めることができる。インターフェースメンバーはデフォルトで public。クラスは複数のインターフェースを実装できる。インターフェースは多重継承をサポートする。

構造体(Struct)

  • 定義:構造体は、小さく軽量なオブジェクトをカプセル化するために使用される値型。
  • 目的:点、色などのデータ構造を表現するのに適している。
  • 特徴:値型でスタックにメモリを割り当てる。継承はサポートしないが、インターフェースは実装できる。構造体のメンバーはデータ、メソッド、イベントなど。構造体はコンストラクタを持てるが、デフォルトのパラメータなしコンストラクタは持てない。構造体はクラスより軽量で、小さなオブジェクトに適している。構造体をコピーすると、そのコピーが作成される。

クラス(Class)

  • 定義:クラスは、オブジェクトのテンプレートを作成するために使用される参照型。
  • 目的:データと動作をカプセル化し、オブジェクト指向プログラミングの基礎。
  • 特徴:参照型でヒープにメモリを割り当てる。継承をサポートし、1つのクラスは別のクラスからのみ継承できる。クラスのメンバーはデータ、メソッド、コンストラクタ、イベントなど。クラスは複数のコンストラクタを持てる。クラスオブジェクトをコピーすると、参照のみがコピーされる。クラスは abstract または sealed で宣言できる。

抽象クラス

定義:抽象クラスはインスタンス化できないクラス。通常は基底クラスとして機能し、共通の機能を定義し部分的に実装するが、少なくとも1つの抽象メソッドを含む。これらの抽象メソッドは派生クラスで実装する必要がある

特徴:抽象クラスは直接インスタンス化できない。抽象クラスは抽象メソッドと具体メソッドを含めることができる。抽象メソッドは実装がなく宣言のみのメソッド。抽象クラスを継承するサブクラスは、サブクラスも抽象でない限り、そのすべての抽象メソッドを実装する必要がある。抽象クラスは、サブクラスによって拡張・実装できる基本的なフレームワークを提供するのにしばしば使用される。

sealed クラス

sealed クラスは継承できないクラス。つまり、他のクラスは sealed クラスから派生できない。これは通常、セキュリティとパフォーマンス最適化のために使用される。

  • クラスが sealed 宣言されると、継承できない。
  • sealed クラスは通常のクラスと同様にインスタンス化して使用できる。
  • sealed クラスは主にさらなる継承を防ぐために使用され、特にフレームワークやライブラリを設計する際、コア機能が変更されないことを確保する。
  • 場合によっては、sealed クラスはランタイムパフォーマンスを向上させることもできる(JIT コンパイラの最適化などが可能になるため)。

クラスと構造体の比較

  • メモリ割り当て:Struct は値型でスタックに割り当てられる。Class は参照型でヒープに割り当てられる。
  • 継承:Class は継承をサポート。Struct は継承をサポートしない。Interface は多重継承の実装に使用される。
  • デフォルトコンストラクタ:Class はカスタムのパラメータなしコンストラクタを持てる。Struct は常にデフォルトのパラメータなしコンストラクタを持つ。
  • インスタンス化:Class はインスタンス化時に new キーワードを使用。Struct は new キーワードを使用する必要がない。
  • 代入動作:Struct を代入すると、その値がコピーされる。Class を代入すると、参照がコピーされる。

ファクトリーパターン

ファクトリーパターン(Factory pattern) は、オブジェクト作成のロジックをクライアントに公開せずにオブジェクトを作成するために使用される生成デザインパターンです。オブジェクトのインスタンス作成のカプセル化メカニズムを提供し、システムをよりモジュール化し、コードの保守性と柔軟性を高めることができます。

クラスが作成するオブジェクトのクラスを知らない場合、またはクラスがそのサブクラスにオブジェクトを作成するクラスを指定することを期待する場合、またはクラスがオブジェクト作成の責任を複数のヘルパークラスの1つに委任し、委任先を情報として隠したい場合に、ファクトリーパターンを使用できます。

種類

ファクトリーパターンには3つの一般的な種類があります。

  1. シンプルファクトリーパターン:GoF 23 デザインパターンには含まれない。集中的なファクトリークラスを使用してすべてのインスタンスを作成する。
  2. ファクトリーメソッドパターン:オブジェクト作成のインターフェースを定義するが、どのクラスをインスタンス化するかはサブクラスに任せる。ファクトリーメソッドは、クラスのインスタンス化をサブクラスに遅延させることを可能にする。
  3. 抽象ファクトリーパターン:具体的なクラスを明示的に指定せずに、関連または依存するオブジェクトのファミリーを作成するためのインターフェースを提供する。

コード

ファクトリーメソッドパターンを例として、異なる種類のロガーを作成するファクトリーがあるとします。

Plain Text// 抽象製品
public interface ILogger
{
    void Log(string message);
}

// 具体製品
public class FileLogger : ILogger
{
    public void Log(string message)
    {
        // ログをファイルに書き込む
    }
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        // ログをコンソールに出力する
    }
}

// 抽象ファクトリー
public abstract class LoggerFactory
{
    public abstract ILogger CreateLogger();
}

// 具体ファクトリー
public class FileLoggerFactory : LoggerFactory
{
    public override ILogger CreateLogger()
    {
        // FileLogger インスタンスを返す
        return new FileLogger();
    }
}

public class ConsoleLoggerFactory : LoggerFactory
{
    public override ILogger CreateLogger()
    {
        // ConsoleLogger インスタンスを返す
        return new ConsoleLogger();
    }
}

// クライアントコード
class Client
{
    static void Main(string[] args)
    {
        LoggerFactory factory = new FileLoggerFactory();
        ILogger logger = factory.CreateLogger();
        logger.Log("This is a log message.");
    }
}

この場合、LoggerFactory はオブジェクト作成のメソッドを定義する抽象ファクトリーです。

具体ファクトリークラスはここでは FileLoggerFactory と ConsoleLoggerFactory で、上記で定義されたメソッドを実装し、具体製品(FileLogger や ConsoleLogger など)を作成するために使用されます。クライアントコードは抽象ファクトリーと製品インターフェースとのみ相互作用するため、新しい種類のロガーを追加してもクライアントコードの変更は不要です。