使用单例模式(或反模式)被认为是不好的实践,因为它使得测试代码变得非常困难,并且依赖关系非常复杂,导致项目在某些时候难以管理。你只能拥有一个每个php进程内的对象固定实例。当为您的代码编写自动化单元测试时,您需要能够替换要测试使用的对象与一个行为可预测的测试 double。当要测试的代码使用单例模式时,您就无法将其替换为一个测试double。
我所知道的最好的组织对象之间交互的方式(如您的数据库对象和其他使用数据库的对象)是反转依赖方向。这意味着您的代码不是从外部源(大多数情况下是您的代码中的静态“get_instance”方法)请求所需的对象,而是在需要之前从外部获得其依赖对象(所需的对象)。通常,您会使用一种依赖注入管理器/容器,例如
来自Symfony项目的此类来组合您的对象。
使用数据库对象的对象将在构造函数中进行注入。它可以通过设置器方法或构造函数进行注入。在大多数情况下(并非所有情况),最好在构造函数中注入依赖项(您的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;
}
}
$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";
$container = array();
$container['db'] = new DB($parameters['dsn'], $parameters['db_user'], $parameters['db_pass']);
$container['fileDb'] = new SomeFileBasedDB($parameters['file_db_path']);
$container['userLoader'] = new UserLoader($container['db']);
$container['userLoader'] = new SomeUserLoaderProxy($container['userLoader'], $container['db']);
$container['userController'] = new UserController($container['fileUserLoader'], $container['viewRenderer']);
Notice how the different classes no not know about each other. There are no direct depencies between them. This is done by not require the actual class in the constructor, but instead require the interface that provides the methods it needs.
That way you can always write replacements for your classes and just replace them in the depency-injection container. You do not have to check the whole codebase because the replacement just has to implement the same interface that is used by all other classes. You know that everything will continue to work because every component using the old class only knows about the interface and calls only methods known by the interface.
P.S.: Please excuse my constant references to the symfony project, it is just what i am most used to. Other project's like Drupal, Propel or Zend probably also have concepts like this.
$db = new PDO... function findUser($db, $id){ $db->query('按ID获取用户'); }
- JimL