Brug af singleton-mønsteret (eller antimønsteret) betragtes som dårlig praksis, fordi det gør det meget svært at teste din kode og afhængighederne meget indviklede, indtil projektet bliver svært at administrere på et tidspunkt. Du kan kun have én fast instans af dit objekt pr. php-proces. Når du skriver automatiserede unit-tests til din kode, skal du være i stand til at erstatte det objekt, den kode du vil teste bruger med en test-double, der opfører sig på en forudsigelig måde. Når koden du vil teste bruger en singleton, så kan du ikke erstatte den med en test double.
Den bedste måde (såvidt jeg ved) at organisere interaktionen mellem objekter (som dit databaseobjekt og andre objekter, der bruger databasen) ville være at vende retningen af afhængighederne. Det betyder, at din kode ikke anmoder om det objekt, den har brug for fra en ekstern kilde (i de fleste tilfælde en global, som den statiske 'get_instance'-metode fra din kode), men i stedet får sit depency-objekt (det den har brug for) serveret udefra før den har brug for det. Normalt ville du bruge en Depency-Injection Manager/Container som denne en fra symfony-projektet at komponere dine objekter.
Objekter, der bruger databaseobjektet, vil få det injiceret ved konstruktion. Det kan injiceres enten ved en setter-metode eller i konstruktøren. I de fleste tilfælde (ikke alle) er det bedre at injicere depency (dit db-objekt) i konstruktøren, fordi det objekt, der bruger db-objektet, aldrig vil være i en ugyldig tilstand.
Eksempel:
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']);
Læg mærke til, hvordan de forskellige klasser ikke kender til hinanden. Der er ingen direkte afhængigheder mellem dem. Dette gøres ved ikke at kræve den faktiske klasse i konstruktøren, men i stedet kræve den grænseflade, der giver de metoder, den har brug for.
På den måde kan du altid skrive erstatninger til dine klasser og bare erstatte dem i depency-injection containeren. Du behøver ikke at tjekke hele kodebasen, fordi erstatningen blot skal implementere den samme grænseflade, som bruges af alle andre klasser. Du ved, at alt vil fortsætte med at fungere, fordi hver komponent, der bruger den gamle klasse, kun kender til grænsefladen og kun kalder metoder, der er kendt af grænsefladen.
P.S.:Undskyld venligst mine konstante referencer til symfoniprojektet, det er lige det, jeg er mest vant til. Andre projekter som Drupal, Propel eller Zend har sikkert også koncepter som dette.