使用单例模式(或反模式)被认为是不好的做法,因为它使测试代码变得非常困难,并且依赖关系非常复杂,直到项目在某个时候变得难以管理。每个 php 进程只能拥有一个固定的对象实例。在为代码编写自动化单元测试时,您需要能够将要测试的代码使用的对象替换为以可预测的方式运行的测试替身。当您要测试的代码使用单例时,您无法将其替换为测试替身。
(据我所知)组织对象(例如数据库对象和使用数据库的其他对象)之间的交互的最佳方法是反转依赖关系的方向。这意味着您的代码不会从外部源请求所需的对象(在大多数情况下是全局对象,例如代码中的静态“get_instance”方法),而是从外部获取其依赖对象(它需要的对象)在它需要它之前。通常您会使用依赖注入管理器/容器,例如这个来自 symfony 项目 http://symfony.com/doc/current/components/dependency_injection/introduction.html#basic-usage来组合你的对象。
使用数据库对象的对象将在构造时注入它。它可以通过 setter 方法或构造函数注入。在大多数情况下(不是全部),最好在构造函数中注入依赖项(您的数据库对象),因为这样使用数据库对象的对象将永远不会处于无效状态。
Example:
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']);
请注意不同的类如何彼此不了解。它们之间没有直接的依赖关系。这是通过在构造函数中不需要实际的类,而是需要提供所需方法的接口来完成的。
这样您就可以随时为您的类编写替换项,然后将它们替换到依赖注入容器中。您不必检查整个代码库,因为替换只需要实现所有其他类使用的相同接口。您知道一切都会继续工作,因为使用旧类的每个组件只知道接口并且只调用接口已知的方法。
P.S.:请原谅我不断引用 symfony 项目,这正是我最习惯的。其他项目如 Drupal、Propel 或 Zend 可能也有类似的概念。