ソフトウェアを他の開発者と議論するたびに、特にWordPress開発のコンテキストでは、シングルトンテーマがポップアップします。 標準テンプレートと見なされている場合でも、それらを避けるべき理由を説明しようとすることがよくあります。
この記事では、なぜコードでシングルトーンを使用すべきではないのか、同様の問題を解決するためにどのような代替策が存在するのか、というトピックについて詳しく説明します。
シングルトンとは何ですか?
シングルトンは、「 デザインパターン:再利用可能なオブジェクト指向ソフトウェアの要素 (著者-The Gang of Four ) 」という本で説明されているソフトウェア開発のデザインパターンです。そのおかげで、デザインパターンがソフトウェア開発ツールとして取り上げられました。
アイデアは、クラスのインスタンスが1つだけ存在する必要があり、そのクラスへのグローバルな単一アクセスポイントを提供するというものです。
これは実際には説明と理解が非常に簡単であり、多くの人にとって、シングルトンはデザインパターンの世界への簡単なエントリーであり、最も人気のあるパターンです。
シングルトンは人気があり、本で説明され標準化された最初のテンプレートの1つでした。 一部の開発者は、それをアンチテンプレートとどのように考えていますか? それは本当にそんなに悪いことができますか?
はい
はい、できます。
しかし、シングルトーンは便利で重要です!
多くの人が2つの関連する概念を混同していることに気付きました。 シングルトンが必要であると言うとき、オブジェクトの1つのインスタンスを異なるインスタンス化操作で実際に使用する必要があります 。 一般に、インスタンスを作成すると、このクラスの新しいインスタンスが作成されます。 ただし、一部のオブジェクトでは、使用場所に関係なく、オブジェクトの同じ共有インスタンスを常に使用する必要があります。
しかし、シングルトンはこのための適切なソリューションではありません 。
混乱は、シングルトンが2つの機能(責任)を1つのオブジェクトに結合するという事実によって引き起こされます 。 データベースに接続するシングルトンがあるとします。 (非常に巧妙に) DatabaseConnection
と呼びましょう。 シングルトンには現在、2つの主要な機能があります。
- 接続管理。
-
DatabaseConnection
インスタンス管理。
人々がシングルトンを選択するのは2番目の機能のためですが、別のオブジェクトがこの問題を解決する必要があります。
一般的なインスタンスに問題はありません。 ただし、これに使用するオブジェクトは、そのような制限の場所ではありません。
以下にいくつかの選択肢を示します。 しかし、まず、シングルトンが引き起こす可能性のある問題を説明します。
シングルトンの問題
シングルトンとソリッド
まず第一に、これは理論的な問題のように見えるかもしれませんが、シングルトンは多くの固体原理に違反しています。
- Sは唯一の責任の原則です 。 前述のように、明らかにシングルトンは彼と矛盾しています。
- O- 開放性/閉鎖性の原則 :オブジェクトは展開のために開かれ、変更のために閉じられるべきです。 シングルトンはアクセスポイントを制御し、拡張機能ではなく自分自身だけを返すため、シングルトンはこの原則に違反します。
- Lは、 Barbara Liskovを置き換える原則です 。オブジェクトは、使用するコードを変更せずに、サブタイプのインスタンスに置き換えることができます。 これは、シングルトンの場合には当てはまりません。オブジェクトの複数の異なるバージョンを持つことは、シングルトンではなくなったことを意味するためです。
- I- インターフェース分離の原理 :多くの特殊なインターフェースは、1つの汎用インターフェースよりも優れています。 これは、シングルトンが直接違反しない唯一の原則ですが、インターフェースの使用を許可しないためです。
- Dは依存関係の反転の原理です 。特定の何かではなく、抽象化のみに依存する必要があります。 この場合、シングルトンの特定のインスタンスにのみ依存できるため、シングルトンは違反します。
シングルトンパターンは、5つの SOLID原則のうち4つに違反しています。 彼はおそらく彼がインターフェースを持つことができれば、5番目を壊したいと思うでしょう...
いくつかの理論的原理のためだけにコードが機能しないと言うのは簡単です。 そして、私自身の経験によると、これらの原則は、ソフトウェアを開発する際に信頼できる最も価値があり信頼できるガイドですが、「これは事実です」という言葉は多くの人に納得させられないことを理解しています。 私たちは、あなたの毎日の練習に対するシングルトンの影響を追跡する必要があります。
シングルトンパターンを使用する
シングルトンを扱うときに遭遇するかもしれない欠点のいくつかを以下に示します。
コンストラクタに引数を渡したり、埋め込むことはできません。 シングルトンへの最初の呼び出しは実際にコンストラクターを実行するだけであり、どのコードが最初にシングルトンに変わるかを事前に知ることはできないため、すべての消費コードでは、同じ引数セットを使用してコンストラクターに渡す必要がありますが、これは多くの場合ほとんど不可能ですが、まったく意味がありません。 その結果、シングルトンは、OOP言語の基本的なインスタンス化メカニズムを役に立たなくします。
シングルトンを使用するコンポーネントをテストするとき、シングルトンをモックすることはできません。 これにより、テスト対象のコードを完全に分離することができないため、正しいユニットテストがほぼ不可能になります。 この問題は、テストするロジックでさえ引き起こされるのではなく、それをラップするインスタンス化の任意の制限によって引き起こされます。
シングルトンは、コードベース全体で使用されるグローバルにアクセス可能な構造であるため、カプセル化の努力は無駄になり、グローバル変数の場合と同じ問題が発生します。 つまり、コードのカプセル化された部分でシングルトンをどのように分離しようとしても、他の外部コードはシングルトンで副作用やバグを引き起こす可能性があります。 そして、適切なカプセル化がなければ、OOPの原理は衰弱します。
DatabaseConnection
シングルトンが突然、最初のデータベース以外の2番目のデータベースに接続する必要があるほど大きくなったWebサイトまたはアプリケーションがある場合、問題が発生します。 アーキテクチャ自体を再検討し、場合によってはコードの大部分を完全に書き直す必要があります。
シングルトンを直接または間接的に使用するすべてのテストは、1つのテストから別のテストに正しく切り替えることはできません。 それらは常にシングルトンを介して状態を保存します。これは、テストが起動の順序に依存する予期しない動作につながる可能性があります。
- シングルトンのインスタンス化は、静的スコープに適した現在のプロセスのスペースに強制的に適用されます。 これは、複数のプロセスまたは分散実行がある場合の並列化の問題を意味します。 これは、シングルトーンが分散システムでの作業を開始するとすぐに壊れる単なる誤った概念である可能性を示唆するはずです。
シングルトンの代替
私はすべてに悪いものを見ている人にはなりたくありませんが、問題の解決策を提供することはできません。 最初にシングルトンの使用を回避する方法を決定するには、アプリケーションのアーキテクチャ全体を評価する必要があると思いますが、シングルトンをすべての要件を満たし、ほとんどの欠点がないメカニズムに簡単に置き換えることができるWordPressの最も一般的な方法のいくつかをお勧めします。 しかし、これについて話す前に、私の提案がすべて妥協である理由に注目したいと思います。
アプリケーション開発用の「理想的なフレームワーク」があります。 理論的には、最適なオプションは、アプリケーションの依存関係ツリー全体を上から下に作成するブートコード内の唯一のインスタンス化呼び出しです。 これは次のように機能します。
- インスタンス
App
(Config
、Database
、Controller
が必要)。 -
App
インスタンスConfig
。 -
App
での展開のためのDatabase
。 -
App
で実装するためのController
インスタンス(Router
、Views
が必要)。 -
Controller
実装するためのRouter
インスタンス(HTTPMiddleware
が必要)。 - ...
1回の呼び出しで、アプリケーションスタック全体が上から下に並べられ、必要に応じて依存関係が注入されます。 このアプローチの目的:
- 各オブジェクトには、必要な依存関係の正確なリストがあり、それらだけがそれらを使用する必要があります。 何かが壊れた場合、その原因となっているコードを簡単に分離できます。
- オブジェクト間に密接な接続はありません;実装された実装を使用するとき、すべてのオブジェクトはインターフェースのみに依存します。
- グローバルステータスはありません。 階層の上位にある各サブツリーは、他のサブツリーから正しく分離されるため、開発者はモジュールAを変更してもモジュールBにバグを作成することはありません。
しかし、どれほど良い音が聞こえても、WordPressでこれを行うことは不可能です。これは、集中化されたコンテナまたは実装メカニズムを提供せず、すべてのプラグイン/テーマが単独でロードされるためです。
アプローチを議論する間、これを覚えておいてください。 WordPressスタック全体が一元化された実装メカニズムによってインスタンス化される理想的なソリューションは、WordPressコアのサポートを必要とするため、利用できません。 以下に説明するすべてのアプローチは、依存関係を導入するのではなく、ロジックから直接参照して依存関係を隠すなど、特定の一般的な欠点が特徴です。
シングルトンコード
他の人と比較するシングルトンアプローチを使用したサンプルコード:
// . final class DatabaseConnection { private static $instance; private function __construct() {} // . public static function get_instance() { if ( ! isset( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } // . public function query( ...$args ) { // . } } // . $database = DatabaseConnection::get_instance(); $result = $database->query( $query );
ここでは、理論的な議論にとって重要ではないため、シングルトーンが頻繁にダウンロードされる実装の詳細をすべて記載していません。
工場方式
ほとんどの場合、シングルトンに関連する問題を回避する最善の方法は、ファクトリメソッドデザインパターンを使用することです。 ファクトリは、他のオブジェクトをインスタンス化することを唯一の義務とするオブジェクトです。 get_instance()
メソッドを使用して独自のインスタンスを作成するDatabaseConnectionManager
代わりに、 DatabaseConnection
オブジェクトをインスタンス化するDatabaseConnection
ます。 一般に、工場は常に目的の施設の新しいインスタンスを生成します。 ただし、要求されたオブジェクトとコンテキストに基づいて、ファクトリは、新しいインスタンスを作成するか、常に共有するかを自分で決定できます。
テンプレートの名前を考えると、PHPコードよりもJavaコードに似ていると思われるかもしれません。そのため、厳密すぎる(そして怠laな)命名規則から逸脱して、より独創的にファクトリーに名前を付けてください。
ファクトリメソッドの例:
// . final class Database { public function get_connection(): DatabaseConnection { static $connection = null; if ( null === $connection ) { // , , . $connection = new MySQLDatabaseConnection(); } return $connection; } } // , (mock) . interface DatabaseConnection { public function query( ...$args ); } // . final class MySQLDatabaseConnection implements DatabaseConnection { public function query( ...$args ) { // . } } // . $database = ( new Database )->get_connection(); $result = $database->query( $query );
ご覧のとおり、コードの消費はそれほど膨大で複雑ではないため、注意点は1つだけです。 DatabaseConnection
、提供するAPIの一部であるため、 DatabaseConnection
代わりに名前を付けることにしました。また、論理的な正確さと簡潔さのバランスを常にとるように努力する必要があります。
工場の所定のバージョンには、1つを除いて、前述のほとんどすべての欠点がありません。
-
DatabaseConnection
オブジェクトとの密接な関係を削除しましたが、代わりにファクトリとの新しい関係を作成しました。 ファクトリは純粋な抽象化であるため、これは問題ではありません。ある時点で「インスタンス化」の概念から離れる必要がある確率は非常に小さいです。 これが発生した場合、OOPパラダイム全体を再考する必要があります。
おそらく、私たちは単一のインスタンス化に自分を強制的に限定することはもはやできないのではないかと思われ始めているでしょう。 DatabaseConnection
実装の共有インスタンスは常に提供しますが、だれでもnew MySOLDatabaseConnection
を実行して追加のインスタンスにアクセスできます。 はい、そうです。これがシングルトンを放棄する理由の1つです。 しかし、これは実際のタスクで常に利点をもたらすとは限りません。ユニットテストなどの基本的な要件に準拠することが不可能になるためです。
静的代理
静的プロキシは、シングルトンを変更できる別の設計パターンです。 これは、ファクトリーよりもさらに近い接続を意味しますが、少なくとも特定の実装ではなく、抽象化による接続です。 考えは、インターフェイスの静的マッピングがあり、これらの静的呼び出しは特定の実装に透過的にリダイレクトされるということです。 したがって、実際の実装との直接的な関係はなく、 静的な代理人が使用する実装の選択方法を決定します。
// . final class Database { public static function get_connection(): DatabaseConnection { static $connection = null; if ( null === $connection ) { // You can have arbitrary logic in here to decide what // implementation to use. $connection = new MySQLDatabaseConnection(); } return $connection; } public static function query( ...$args ) { // Forward call to actual implementation. self::get_connection()->query( ...$args ); } } // , (mock) . interface DatabaseConnection { public function query( ...$args ); } // . final class MySQLDatabaseConnection implements DatabaseConnection { public function query( ...$args ) { // . } } // . $result = Database::query( $query );
ご覧のとおり、 静的な代替は非常に短くクリーンなAPIを作成します。 欠点には、コードとクラスシグネチャの間に密接な関係があるという事実が含まれます。 適切な場所で使用すると、特定の実装ではなく、直接制御できる抽象化との接続であるため、これは特別な問題を引き起こしません。 1つのデータベースのコードを、必要と考える他のデータベースのコードに置き換えることができ、実装はテスト可能な完全に正常なオブジェクトのままです。
WordPressプラグインAPI
WordPressプラグインAPIは、プラグインを介してグローバルアクセスを提供できるようにするために使用される場合、シングルトーンを置き換えることができます。 これは、WordPressの制限を考慮した最もクリーンなソリューションです。コードのインフラストラクチャとアーキテクチャ全体がWordPressプラグインAPIに関連付けられていることに注意してください。 異なるフレームワークでコードを再利用する場合は、この方法を使用しないでください。
// , (mock) . interface DatabaseConnection { const FILTER = 'get_database_connection'; public function query( ...$args ); } // . class MySQLDatabaseConnection implements DatabaseConnection { public function query( ...$args ) { // . } } // . $database = new MySQLDatabaseConnection(); add_filter( DatabaseConnection::FILTER, function () use ( $database ) { return $database; } ); // . $database = apply_filters( DatabaseConnection::FILTER ); $result = $database->query( $query );
主なトレードオフの1つは、アーキテクチャがWordPressプラグインAPIに直接結び付けられることです。 Drupalサイトにプラグイン機能を提供する予定がある場合は、コードを完全に書き直す必要があります。
もう1つの考えられる問題は、WordPressフックのタイミングに依存していることです。 これにより、タイミングに関連するバグが発生する可能性があり、多くの場合、再現および修正が困難です。
サービスロケーター
サービスロケーターは、 Inversion of Control Containerの 1つの形式です。 一部のサイトでは、この方法をアンチパターンとして説明しています。 一方で、これは真実ですが、他方で、上で議論したように、ここで行われたすべての推奨事項は妥協と見なすことができるだけです。
サービスロケーターは、他の場所で実装されたサービスへのアクセスを提供するコンテナーです。 コンテナは、ほとんどの場合、識別子に関連付けられたインスタンスのコレクションです。 より高度なサービスロケーターの実装では、遅延インスタンス化や代替生成などの機能を導入できます。
// , . interface Container { public function has( string $key ): bool; public function get( string $key ); } // . class ServiceLocator implements Container { protected $services = []; public function has( string $key ): bool { return array_key_exists( $key, $this->services ); } public function get( string $key ) { $service = $this->services[ $key ]; if ( is_callable( $service ) ) { $service = $service(); } return $service; } public function add( string $key, callable $service ) { $this->services[ $key ] = $service; } } // , (mock) . interface DatabaseConnection { public function query( ...$args ); } // . class MySQLDatabaseConnection implements DatabaseConnection { public function query( ...$args ) { // . } } // . $services = new ServiceLocator(); $services->add( 'Database', function () { return new MySQLDatabaseConnection(); } ); // . $result = $services->get( 'Database' )->query( $query );
ご想像のとおり、 $services
インスタンスへのリンクを取得する問題はなくなりませんでした。 この方法を前の3つと組み合わせることで解決できます。
- 工場で:
$result = ( new ServiceLocator() )->get( 'Database' )->query( $query );
- 静的代替の場合 :
$result = Services::get( 'Database' )->query( $query );
- WordPressプラグインAPIを使用する場合:
$services = apply_filters( 'get_service_locator' ); $result = $services->get( 'Database' )->query( $query );
ただし、シングルトンアンチパターンの代わりにサービスロケーターアンチパターンを使用するかどうかの質問にはまだ答えがありません... サービスロケーターに問題があります。依存関係を「隠し」ます。 正しいコンストラクター実装を使用するコードベースを想像してください。 この場合、特定のオブジェクトのコンストラクターを見るだけで、そのオブジェクトが依存しているオブジェクトをすぐに理解できます。 オブジェクトがサービスロケーターへのリンクにアクセスできる場合、依存関係のこの明示的な解決をバイパスし、実際のロジックからオブジェクトにリンクを抽出することができます(したがって、依存し始めます)。 これは、 サービスロケーターが依存関係を「隠す」と言うときの意味です。
しかし、WordPressのコンテキストを考えると、最初から完全なソリューションが利用できないという事実を受け入れる必要があります。 コードベース全体に依存関係の正しい実装を実装する技術的な機能はありません。 これは、いずれにせよ、妥協を模索する必要があることを意味します。 サービスロケーターは理想的なソリューションではありませんが、このテンプレートはレガシーコンテキストに適合し、少なくともコードベースに散らばるのではなく、1か所ですべての「妥協点」を収集できます。
依存性注入
独自のプラグインでのみ作業し、他のプラグインへのオブジェクトへのアクセスを提供する必要がない場合は、幸運です。この依存性注入を使用して、依存性へのグローバルアクセスを回避できます。
// , (mock) . interface DatabaseConnection { public function query( ...$args ); } // . class MySQLDatabaseConnection implements DatabaseConnection { public function query( ...$args ) { // . } } // . class Plugin { private $database; public function __construct( DatabaseConnection $database ) { $this->database = $database; } public function run() { $consumer = new Consumer( $this->database ); return $consumer->do_query(); } } // . // . class Consumer { private $database; public function __construct( DatabaseConnection $database ) { $this->database = $database; } public function do_query() { // . // . return $this->database->query( $query ); } } // . $database = new MySQLDatabaseConnection(); $plugin = new Plugin( $database ); $result = $plugin->run();
, , , .
, , , .
, (wiring) . ( Dependency Injector ) ( ), .
, ( /, ):
// , (resolving) DatabaseConnection. $injector->alias( DatabaseConnection::class, MySQLDatabaseConnection::class ); // , DatabaseConnection . $injector->share( DatabaseConnection::class ); // Plugin, , . $plugin = $injector->make( Plugin::class );
組み合わせ
, , , .
, , , :
- .
- — (service provider), .
- —> .
- —> (service location).
- —> , .
おわりに
. WordPress', .
, , , , .
, — , !