【问题标题】:Singleton v Single Instance DB Connection in PHPPHP 中的 Singleton v 单实例数据库连接
【发布时间】:2014-07-31 03:48:55
【问题描述】:

我将开始使用 PHP 自学 OOP。

我正在创建几个小型 Web 应用程序,并遵循了很多教程,这些教程要么通过单例创建数据库(使用 PDO),要么通过传递全局。我读到这些几乎是一回事,都应该像瘟疫一样避免。

所以我观看了关于干净代码的 Google 技术讲座,并且阅读了几乎所有关于依赖注入等的 SO 文章。我有几个问题。

  1. 干净的代码视频建议您不要在构造函数中“工作”。这是关于业务逻辑的“工作”。 IE。如果我的班级的工作是创建另一个对象,那是一种好的“工作”吗?

例如,为了符合单一责任类,我创建了三个。

  1. DB 类 - 实际连接到数据库。
  2. DBFactory 类 - 创建连接到数据库的 DB 对象。
  3. 类 DBInstance - 返回 DBFactory 创建的 PDO 对象的单个实例。

请注意,我正在尝试创建单个实例,而不创建单例模式。

所以我尝试将每个类的依赖项向上传递。我发现自己必须创建所有对象(从 DB 向下),以便注入依赖项。出于某种原因,我认为它会以另一种方式工作,我会创建第一个对象,它会为我创建第二个对象等等。我显然错过了什么?

希望这对其他人也有帮助 - 似乎有无数关于这些东西和数据库的问题,但很少有好的例子。

(我应该提到这确实有效,我确实从数据库中获得了酒店名称列表!)

TestCode.php

include './classes/DB.php';
include './classes/DBFactory.php';
include './classes/DBInstance.php';
include './classes/Location.php';

$db = new DB;
$dbfactory = new DBFactory($db);
$dbinstance = new DBInstance($dbfactory);
$dbh = $dbinstance->getDbInstance();

//Example business logic
$location_names = Location::getLocationNames($dbh);
print_r($location_names);

类 DB.php:

class DB {

    private $_dbhost = 'myhost';
    private $_dbname = 'myname';
    private $_dbuser = 'myuser';
    private $_dbpass = 'mypass';

    private $_error;

    public function connect() {
        try {
            return new PDO("mysql:host=$this->_dbhost;dbname=$this->_dbname", 
                $this->_dbuser, $this->_dbpass);
        }

        catch (PDOException $e) {
            $this->_error = 'Error! ' . $e->getMessage() . '<br />';
            die();
        }
    }

    public function getError() {
        if (isset($this->_error)) {
            return $this->_error;
        }
    }
}

类 DBFactory.php

class DBFactory {

    private $_dbh;

    public function __construct(DB $db) {
        $this->_dbh = $db;
    }

    public function Create() {
        return $this->_dbh->Connect();
    }
}

类 DBInstance.php

class DBInstance {

    private static $_dbinstance;

    public function __construct(DBFactory $dbfactory) {

        if (!isset(self::$_dbinstance)) {
            self::$_dbinstance = $dbfactory->Create();
        }
    }

    public function getDbInstance() {
        return self::$_dbinstance;
    }
}

【问题讨论】:

  • @félix-gagnon-grenier 我是否成功创建了单个实例,而没有创建单例模式?有没有更好的方法来创建一个新的数据库处理程序,同时仍然使用依赖注入,而不必像我在测试类中那样实例化所有对象?

标签: php oop design-patterns singleton database-connection


【解决方案1】:

您的代码似乎按照您的意愿行事。但也许我们可以通过继承使用更少的对象实例化,也许我们可以避免实例化类中的静态属性。

还有关于使用能够处理多个连接但支持使用它的单个实例的依赖注入模式。先例,后类

$params = array
 ('host'=>'localhost',
  'db'=>'ice',
  'user'=>'kopitar',
  'pass'=>'topnet',
  'charset'=>'utf8'); // passing the charset explicitely is great

$handle = new handle($params);
$db = $handle->getInstance();

我们可以将$db 传递给我们的函数

$location_names = Location::getLocationNames($db); 

或整个 $handle。只要 $handle 没有被重构,它总是会返回相同的数据库连接。

$location_names = Location::getLocationNames($handle);

如果我想重建我需要整个$handle

$handle->__construct(/* params but with another database infos */);
$db2 = $handle->getInstance();

至于类,我认为我们希望参数来自实例化的类,因此我们可以稍后更改它们。

class db {
  function __construct($params) {
    foreach ($params as $param => $value) {
      $this->{$param} = $value; // assigns the connections infos
    }
  }
  protected function connect() {
    $dsn = 'mysql:host='.$this->host.';dbname='.$this->db.';charset='.$this->charset;
    return new PDO($dsn,$this->user,$this->pass);
  }
}

工厂从参数创建一个连接并将其传递给其他东西,好的工厂

class factory extends db {
  protected function create() {
    return $this->connect();
  }
}

现在我们想让我们的对象保持它的连接,只要我们不重建它。所以我们把它交给实例

class instance extends factory {
  function instantiate() {
    $this->instance = $this->create();
  }
}

最后但同样重要的是,我们返回实例的句柄。它可能在实例类中...........

但我想拥有四个,但没有真正的理由不这样做。

class handle extends instance {
  function __construct($params) {
    db::__construct($params);
    $this->instantiate(); // when we construct a handle, we assign an instance to the instance property
  }
  function getInstance() {
    return $this->instance;
  }
}

【讨论】:

  • 感谢@félix-gagnon-grenier,这看起来确实是一个很大的改进。我一直在尝试将它与我的代码集成。我唯一想知道的是它是否会引入不良的依赖关系?例如,在子类中说 parent::__construct 是否等同于在构造函数中实例化不是子类的对象?例如。 parent::__construct($params) 将与 $handle = new DB 或类似的相同。我只是对这一切感到好奇,我觉得这是一个有趣的挑战。感谢您的宝贵时间!
  • @aussie_aj 我认为你是对的,通过parent::__construct() 必须与直接调用db::__construct() 相同,如果不是更昂贵的话......我会测试并更新如果真的
  • 完成。这样会更酷,而且它确实构建的东西更少。
  • 是的@félix-gagnon-grenier 这对我来说真的很好,而且超级简单。我想我会在我的应用程序中采用它。感谢您的指导!
【解决方案2】:

亲吻

不要让事情变得比实际更复杂,这当然只是我的观点,但在我看来,您正在为其他人说在某些情况下可能存在的问题构建复杂的解决方案。 Php 不是多线程的,因此最大的争论之一是过火了。 (在极少数情况下可能)

我在我的数据库连接中使用单例已经有 15 年了,从来没有遇到过问题,我确实使用了不同的连接,让一个单例处理多个连接实例,但是无论如何......它工作得很好而且每个查看代码的人..都直接理解它。 我没有使用全局变量,因为它们可以被覆盖并且很难预测(当它包含正确的对象时,以及它们何时/为什么不包含)

使用 OOP 让您的代码更简洁、更易于使用和更灵活。 不要使用它来修复不存在的问题,并使您的代码更复杂,因为其他人告诉您这样做。

一个非常简单的 db-connection 单例类处理多个不同连接的示例。

class singleton{
  private static $_instances=array();

  public static function getInstance($connectionName){
     if(!isset(self::$_instance[$connectionName]){
        self::$_instance[$connectionName]=self::_getConnection($connectionName);
     }
     return self::$_instance[$connectionName];
  }

}

只有我的 2 美分

【讨论】:

  • 感谢@ikkuh 的回复。但是您不是刚刚使用单例创建了一个全局/全局状态吗?任何人都可以随时访问它?
  • 是的,我做到了;)只要您不是多线程,这是完全有效的。我所做的是拥有单独的模型类来保存实际查询,它们调用这个类来访问单例。
  • 只是为了确定,你不应该做类似的事情 while(singleton::getInstance()->doSomething()){ singleton::getInstance()->doSomethingElse() } 因为这确实会扭曲全局状态。但是如果你正确地使用你的 PDO 对象并且它应该的方式,这永远不会发生;)
  • 虽然保持简单很重要,但我认为将任务封装在特定的类中一个好主意,只是为了可维护性和模块化,即使你的类只做一样东西。我认为它是 UNIX 意识形态中的一个重要准则。遵循 UNIX 准则的系统,例如那些不同的 BSD 或 Solaris,往往非常高效。
【解决方案3】:

如果你有一个单例,为什么还要有工厂?这是不必要的。

这是一场永无休止的辩论,但我主张不要使用单例进行数据库连接。

就大多数应用程序而言,您只有一个数据通道,您可以认为您的数据库连接是唯一的,但这可能并不总是正确的。

实际上,创建单例数据库连接所付出的努力甚至比创建常规连接还要大。

另外,您的类DB 是不可配置的,因此,当您的连接参数更改时,您需要更改它。而且我认为DB 是一个非常糟糕的名字。

我宁愿称它为Storage 并执行以下操作:

inteface Storage {
    public function insert($container, array $data);
    public function update($container, array $data, $where);
    public function delete($container, $where);
    public function getAll($container);
    public function getOne($identifier);
}

final class PdoStorage implements Storage {
    private $dbh;
    private $dsn;
    private $user;
    private $pswd;

    public function __construct($dsn, $user, $pswd) {
        $this->dsn = $dsn;
        $this->user = $user;
        $this->pswd = $pswd;
    }

    // Lazy Initialization
    private function connect() {
        if ($this->dbh === null) 
            $this->dbh = new PDO($this->dsn, $this->user, $this->pswd);
    }

    public function insert($container, array $data) {
        $this->connect();

        // ... omitted for brevity
    }
}

现在,当您需要数据库存储时,您可以这样做:

$someObj = new SomeClass(new PdoStorage(...));

现在您可能想知道是否需要为依赖于它的每个对象创建一个PdoStorage

答案是:

现在您可以使用工厂来简化您的生活。

class SomeFactory {
    private $defaultStorage;

    public function __construct(Storage $storage) {
        $this->defaultStorage = $storage;
    }

    public function create($type) {
        // Somehow fetches the correct class to instantiate and put it into $class variable , for example... and then

        return new $class($this->defaultStorage); // Or you'd better do this with reflection
    }
}

$factory = new SomeFactory(new PdoStorage(...));
$factory->create('SomeClass');

这样,如果需要,您可以只拥有一个或多个数据库连接器。

【讨论】:

  • 感谢@henrique-barcelos 的回复。我有一个单独的工厂与单例的原因是我想分离创建数据库对象的责任并仅返回数据库的单个实例。 IE。我想要一个数据库实例,而不使用单例模式。这样以后,如果我确实想使用多个实例,我可以更改 getInstance 方法。 butunclebob.com/ArticleS.UncleBob.SingletonVsJustCreateOne
  • 但是你可以直接注入单例,你不需要工厂......我不是说这是你应该做的,我也完全不同意......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-10-12
  • 1970-01-01
  • 2011-02-03
  • 1970-01-01
  • 2014-01-05
  • 2012-09-06
  • 1970-01-01
相关资源
最近更新 更多