シングルトンパターン(またはアンチパターン)を使用すると、コードのテストが非常に難しくなり、プロジェクトの管理が困難になるまで依存関係が非常に複雑になるため、悪い習慣と見なされます。 php-processごとにオブジェクトの固定インスタンスを1つだけ持つことができます。コードの自動化された単体テストを作成するときは、テストするコードが使用するオブジェクトを、予測可能な方法で動作するtest-doubleに置き換えることができる必要があります。テストするコードがシングルトンを使用している場合、それをテストダブルに置き換えることはできません。
オブジェクト(データベースオブジェクトやデータベースを使用する他のオブジェクトなど)間の相互作用を整理するための(私の知る限り)最良の方法は、依存関係の方向を逆にすることです。つまり、コードは外部ソースから必要なオブジェクト(ほとんどの場合、コードの静的な'get_instance'メソッドのようなグローバルオブジェクト)を要求していませんが、代わりにその依存オブジェクト(必要なオブジェクト)を外部から提供しますそれが必要になる前に。通常は、thisのようなDepency-InjectionManager/Containerを使用します。 symfonyプロジェクトからの1つ オブジェクトを作成します。
database-objectを使用するオブジェクトは、構築時に注入されます。セッターメソッドまたはコンストラクターのいずれかで注入できます。ほとんどの場合(すべてではありません)、コンストラクターに依存関係(db-object)を注入することをお勧めします。そうすれば、db-objectを使用するオブジェクトが無効な状態になることはありません。
例:
interface DatabaseInterface
{
function query($statement, array $parameters = array());
}
interface UserLoaderInterface
{
public function loadUser($userId);
}
class DB extends PDO implements DatabaseInterface
{
function __construct(
$dsn = 'mysql:host=localhost;dbname=kida',
$username = 'root',
$password = 'root',
) {
try {
parent::__construct($dsn, $username, $password, array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'");
parent::setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
echo $e->getMessage();
}
}
function query($statement, array $parameters = array())
{
# ...
}
}
class SomeFileBasedDB implements DatabaseInterface
{
function __construct($filepath)
{
# ...
}
function query($statement, array $parameters = array())
{
# ...
}
}
class UserLoader implements UserLoaderInterface
{
protected $db;
public function __construct(DatabaseInterface $db)
{
$this->db = $db;
}
public function loadUser($userId)
{
$row = $this->db->query("SELECT name, email FROM users WHERE id=?", [$userId]);
$user = new User();
$user->setName($row[0]);
$user->setEmail($row[1]);
return $user;
}
}
# the following would be replaced by whatever DI software you use,
# but a simple array can show the concept.
# load this from a config file
$parameters = array();
$parameters['dsn'] = "mysql:host=my_db_server.com;dbname=kida_production";
$parameters['db_user'] = "mydbuser";
$parameters['db_pass'] = "mydbpassword";
$parameters['file_db_path'] = "/some/path/to/file.db";
# this will be set up in a seperate file to define how the objects are composed
# (in symfony, these are called 'services' and this would be defined in a 'services.xml' file)
$container = array();
$container['db'] = new DB($parameters['dsn'], $parameters['db_user'], $parameters['db_pass']);
$container['fileDb'] = new SomeFileBasedDB($parameters['file_db_path']);
# the same class (UserLoader) can now load it's users from different sources without having to know about it.
$container['userLoader'] = new UserLoader($container['db']);
# or: $container['userLoader'] = new UserLoader($container['fileDb']);
# you can easily change the behaviour of your objects by wrapping them into proxy objects.
# (In symfony this is called 'decorator-pattern')
$container['userLoader'] = new SomeUserLoaderProxy($container['userLoader'], $container['db']);
# here you can choose which user-loader is used by the user-controller
$container['userController'] = new UserController($container['fileUserLoader'], $container['viewRenderer']);
異なるクラスがお互いを知らないことに注意してください。それらの間に直接の依存関係はありません。これは、コンストラクターに実際のクラスを必要とせず、代わりに必要なメソッドを提供するインターフェースを必要とすることによって行われます。
そうすれば、いつでもクラスの置換を記述して、それらをdepency-injectionコンテナーに置換することができます。置換は他のすべてのクラスで使用されるのと同じインターフェースを実装するだけなので、コードベース全体をチェックする必要はありません。古いクラスを使用するすべてのコンポーネントはインターフェースについてのみ認識し、インターフェースによって認識されているメソッドのみを呼び出すため、すべてが引き続き機能することがわかります。
追伸:symfonyプロジェクトへの私の絶え間ない言及を許してください、それは私が最も慣れているものです。 Drupal、Propel、Zendなどの他のプロジェクトにも、おそらくこのような概念があります。