在 PHP 中是否存在具有数据库访问权限的单例用例?

我通过 PDO 访问我的 MySQL 数据库。我正在设置进入数据库的权限,我的第一次尝试是使用以下方法:

我首先想到的是 global:

$db = new PDO('mysql:host=127.0.0.1;dbname=toto', 'root', 'pwd');


function some_function() {
global $db;
$db->query('...');
}

这被认为是一个不好的做法。经过一点点的搜索,我最终得到了 单例模式

“适用于需要一个类的单个实例的情况。”

根据手册中的例子,我们应该这样做:

class Database {
private static $instance, $db;


private function __construct(){}


static function singleton() {
if(!isset(self::$instance))
self::$instance = new __CLASS__;


return self:$instance;
}


function get() {
if(!isset(self::$db))
self::$db = new PDO('mysql:host=127.0.0.1;dbname=toto', 'user', 'pwd')


return self::$db;
}
}


function some_function() {
$db = Database::singleton();
$db->get()->query('...');
}


some_function();

为什么我需要相对较大的类时,我可以做到这一点?

class Database {
private static $db;


private function __construct(){}


static function get() {
if(!isset(self::$db))
self::$db = new PDO('mysql:host=127.0.0.1;dbname=toto', 'user', 'pwd');


return self::$db;
}
}


function some_function() {
Database::get()->query('...');
}


some_function();

这最后一个工作完美,我不需要担心 $db了。

我怎样才能创建一个更小的单例类,或者是否有一个我在 PHP 中缺少的单例用例?

42858 次浏览

我看不出这有什么意义。如果你以这样的方式实现这个类,连接字符串被作为构造函数的一个参数,并维护一个 PDO对象列表(每个唯一的连接字符串一个) ,那么可能会有一些好处,但是在这个实例中实现单例似乎是一个毫无意义的练习。

在我看来,你没有错过任何东西。这个例子有很大的缺陷。 如果单例类具有一些非静态实例变量,那么结果会有所不同。

在您的示例中,您处理的是一条看似不变的信息。对于这个例子来说,一个 Singleton 可能有点过了,在类中使用一个静态函数就足够了。

更多的想法: 你可能正在经历一个为了模式而实现模式的案例,而你的直觉告诉你“不,你不必这样做”,因为你已经说明了原因。

但是: 我们不知道您的项目的规模和范围。如果这是一个简单的代码,也许可以扔掉,这是不太可能需要改变,那么是的,继续使用静态成员。但是,如果您认为您的项目可能需要扩展,或者需要为将来的维护编码做准备,那么,是的,您可能需要使用单例模式。

你的解释是正确的。单身人士有他们的位置,但过度使用。通常,访问静态成员函数就足够了(特别是当您不需要以任何方式控制构造时间时)。更好的方法是,只需在名称空间中放入一些自由函数和变量。

编程时没有“对”和“错”,只有“好的实践”和“坏的实践”。

单例通常被创建为一个类,以便以后重用。它们需要以这样的方式创建,即程序员在午夜醉醺醺地编写代码时不会意外地实例化两个实例。

如果有一个简单的小类 不应该被实例化不止一次,那么就不需要使用 需要来使它成为一个单例。这只是一个安全网,如果你这样做。

拥有全局对象并不是 一直都是的坏习惯。如果您知道您将在全球/所有地方/所有时间使用它,那么它可能是为数不多的例外之一。然而,全球化通常被认为是“不良做法”,就像 goto被认为是不良做法一样。

许多人认为单例被认为是 反模式,因为它们实际上只是美化了的全局变量。在实践中,一个类只有一个实例是 有需要的情况相对较少; 通常只有一个实例是 足够,在这种情况下,完全没有必要将其实现为单例。

回答这个问题,你是对的,单身在这里是过度杀戮。一个简单的变量或函数就可以了。然而,一个更好的(更健壮的)方法是使用 依赖注入来完全消除对全局变量的需求。

首先,我想说的是,我并没有发现这种单例模式有多大用处。为什么要在整个应用程序中保留单个对象?特别是对于数据库,如果我想连接到另一个数据库服务器该怎么办?我每次都要断开和重新连接?不管怎样。

在应用程序中使用全局单例模式有以下几个缺点:

  • 很难进行单元测试
  • 依赖注入问题
  • 可以创建锁定问题(多线程应用程序)

使用静态类代替单例实例也提供了一些相同的缺点,因为单例实例的最大问题是静态 getInstance方法。

你可以不使用传统的 getInstance方法来限制一个类的数量:

class Single {


static private $_instance = false;


public function __construct() {
if (self::$_instance)
throw new RuntimeException('An instance of '.__CLASS__.' already exists');


self::$_instance = true;
}


private function __clone() {
throw new RuntimeException('Cannot clone a singleton class');
}


public function __destruct() {
self::$_instance = false;
}


}


$a = new Single;
$b = new Single; // error
$b = clone($a); // error
unset($a);
$b = new Single; // works

这将有助于上面提到的第一点: 单元测试和依赖注入,同时仍然确保类的单个实例存在于您的应用程序中。例如,您可以将生成的对象传递给您的模型(MVC 模式) ,供它们使用。

好吧,我刚开始工作的时候也想过这个问题。用不同的方法实现了它,并提出了两个选择不使用静态类的理由,但这两个理由都很重要。

一个是你会发现,通常情况下,你绝对肯定你不会有多于一个实例的东西,你最终会有第二个。您最终可能会有第二个监视器、第二个数据库、第二个服务器——无论是什么。

当这种情况发生时,如果您使用了静态类,那么与使用了单例相比,您所处的重构要糟糕得多。单例模式本身是一种可疑的模式,但它很容易转换为智能工厂模式——甚至可以转换为使用依赖注入模式,而不会遇到太多麻烦。例如,如果您的单例是通过 getInstance ()获得的,那么您可以很容易地将其更改为 getInstance (database aseName)并允许多个数据库——不需要更改其他代码。

第二个问题是测试(老实说,这和第一个问题是一样的)。有时需要用模拟数据库替换数据库。实际上,这是数据库对象的第二个实例。这在静态类中要比在单例类中困难得多,您只需要模拟 getInstance ()方法,而不是静态类中的每一个方法(在某些语言中这是非常困难的)。

这实际上可以归结为习惯——当人们说“全球化”是不好的,他们有很好的理由这样说,但它可能并不总是显而易见,直到你自己找到了问题所在。

你能做的最好的事情就是询问(就像你做的那样) ,然后做出选择,观察你的决定的后果。拥有解释代码随时间演化的知识要比一开始就正确地进行代码演化重要得多。

简单地考虑一下您的解决方案与 PHP 文档中提供的解决方案有何不同。实际上,只有一个“小”区别: 您的解决方案为 getter 的调用者提供了一个 PDO实例,而文档中的解决方案为 Database::singleton的调用者提供了一个 Database实例(然后他们使用该实例上的 getter 来获得一个 PDO实例)。

那么我们得出了什么结论呢?

  • 在文档代码中,调用方获得一个 Database实例。Database类可能公开(实际上,如果您遇到所有这些麻烦,它将公开 应该)一个比它所包装的 PDO对象更丰富或更高级的接口。
  • 如果更改实现以返回另一种比 PDO更丰富的类型,那么这两种实现是等价的。遵循手动实现没有任何好处。

在实践方面,Singleton 是一个颇具争议的模式。这主要是因为:

  • 它被过度使用了。新手程序员理解 Singleton 要比理解其他模式容易得多。然后,他们继续将他们新发现的知识应用到每一个地方,即使手头的问题没有 Singleton 也能更好地解决(当你拿着锤子时,一切看起来都像钉子)。
  • 根据不同的编程语言,以密不透风、不泄漏的方式实现单例可能是一项艰巨的任务(特别是如果我们有高级场景: 一个单例依赖于另一个单例,单例可以被销毁和重新创建,等等)。试着搜索一下“最终的”c + + 单例实现,我敢说(我拥有 Andrei Alexandrescu 开创性的现代 c + + 设计,它记录了很多混乱)。
  • 在编写 Singleton 代码和编写访问它的代码时,它都增加了额外的工作负载,这些工作负载不需要遵循对程序变量执行操作的一些自我约束即可完成。

因此,作为最后的结论: 您的单例模式很好。在大多数情况下,完全不使用 Singleton 也是可以的。

单例模式在 PHP 中几乎没有用处——如果不是说没有用处的话。

在对象位于共享内存中的语言中,可以使用单例来降低内存使用。不需要创建两个对象,而是从全局共享应用程序内存中引用现有实例。在 PHP 中没有这样的应用程序内存。在一个请求中创建的单例对应于该请求。在另一个同时完成的请求中创建的 Singleton 仍然是一个完全不同的实例。因此,Singleton 的两个主要目的之一在这里是不适用的。

此外,在应用程序中概念上只存在一次的许多对象不一定需要语言机制来实现这一点。如果 需要只有一个实例,那么 不要实例化另一个。只有当您使用 可能没有其他实例时,例如当您创建第二个实例时,小猫死亡,您才可能拥有一个有效的 Singleton 用例。

另一个目的是在同一个请求中拥有一个实例的全局访问点。虽然这听起来可能是可取的,但实际上并非如此,因为它创建了与全局范围的耦合(就像任何全局变量和静态变量一样)。这使得单元测试更加困难和您的应用程序一般较难维护。有一些方法可以减轻这种情况,但是一般来说,如果您需要在许多类中使用相同的实例,请使用 依赖注入

有关更多信息,请参见我的 PHP 中的单例——为什么它们不好以及如何从应用程序中消除它们幻灯片。

甚至连单例模式的发明者之一 Erich Gamma也对如今的这种模式表示怀疑:

“我赞成放弃 Singleton。它的用途几乎总是一种设计气味”

进一步阅读

如果在上述情况之后,你仍然需要帮助来决定:

Singleton Decision Diagram

在 PHP 中谁需要单例?

请注意,几乎所有反对单例模式的理由都来自技术层面——但是它们的范围也非常有限。特别是 PHP。首先,我将列出一些使用单例的理由,然后我将分析使用单例的反对意见。首先,需要它们的人:

编写大型框架/代码库的人,将在许多不同的环境中使用,将不得不使用以前存在的,不同的框架/代码库,必须实现许多不同的,不断变化的,甚至是来自客户/老板/管理层/单位领导的异想天开的请求。

单例模式是自我包容的。这样做之后,单例类在包含它的任何代码中都是严格的,它的行为与创建其方法和变量的方式完全相同。并且在给定的请求中总是相同的对象。因为它不能被创建两次成为两个不同的对象,所以你知道一个单例对象在代码的任何给定点是什么——即使这个单例对象被插入到两个、三个不同的、古老的、甚至意大利面条式的代码库中。因此,就开发目的而言,它使开发变得更加容易——即使有很多人在那个项目中工作,当你在任何给定的代码库中看到一个单例被初始化时,你知道它是什么,它做什么,它是如何做的,以及它处于什么状态。如果它是传统类,则需要跟踪该对象最初在哪里创建,在代码中的那一点之前在其中调用了哪些方法,以及它的特定状态。但是,如果在这里删除一个单例模式,并且在编写单例模式时删除了正确的调试和信息方法以及跟踪,那么您就确切地知道它是什么了。因此,对于那些不得不使用不同代码库的人来说,它使得集成代码变得更加容易,因为集成代码的必要性在早期是用不同的理念完成的,或者是由您没有接触过的人完成的。(也就是说,供应商-项目-公司-无论是有没有更多,没有任何支持)。

- 需要与第三方 应用程式界面、服务和网站合作的人员

如果你仔细看看,这与早期的情况没有太大的不同-第三方 API,服务,网站,就像外部,孤立的代码库,你无法控制。一切皆有可能。因此,通过一个单例会话/用户类,你可以管理任何类型的会话/授权实现从第三方提供者,如 OpenID脸书推特和许多更多-你可以做这些全部在同一时间从相同的单例对象-这是很容易访问,在一个已知的状态在任何给定的点,你插入的代码。您甚至可以在自己的网站/应用程序中为同一个用户创建多个不同的第三方 API/服务的多个会话,并对它们做任何您想做的事情。

当然,通过使用普通类和对象,所有这些也可以与传统方法相协调——这里的问题是,单例模式更整洁、更简洁,因此,与传统类/对象在这种情况下的使用相比,单例模式更容易管理/测试。

需要快速发展的人

单例类似全局的行为使得构建任何类型的代码都变得更加容易,因为一旦你很好地构建了单例类,那么已建立的、成熟的和 set 方法将很容易在任何地方、任何时间以一致的方式使用。使你的类成熟需要一些时间,但是在那之后,它们是坚如磐石的、一致的、有用的。你可以在一个单例模式中使用任何你想要的方法,虽然这可能会增加对象的内存占用,但是它可以为快速开发节省更多的时间——一个你在一个给定的应用程序实例中没有使用的方法可以在另一个集成的应用程序中使用,你只需要修改一下客户端/老板/项目经理要求的一个新特性。

你明白我的意思了,现在让我们继续讨论反对单例和 邪恶的十字军东征反对有用的东西:

最主要的反对意见是,这使得测试更加困难。

实际上,在某种程度上确实如此,即使通过采取适当的预防措施并将调试例程编码到单例程序中可以很容易地减轻这种情况,并且意识到您将调试一个单例程序。但是,你看,这和其他任何编码哲学/方法/模式都没有太大的不同——只是,单例相对来说还比较新,而且还没有广泛应用,所以当前的测试方法最终与它们相比是不兼容的。但这在编程语言的任何方面都没有什么不同——不同的风格需要不同的方法。

这个反对意见有一点是站不住脚的,它忽略了这样一个事实,即开发应用程序不是为了“测试”,而且测试不是进入应用程序开发的唯一阶段/过程。应用程序是为生产用途而开发的。正如我在“谁需要单机”一节中所解释的,单机可以大大减少代码工作的复杂性,因为它必须在许多不同的代码库/应用程序/第三方服务内部工作。在测试中可能损失的时间,是在开发和部署中获得的时间。这在第三方认证/应用/集成的时代尤其有用—— Facebook、 Twitter、 OpenID 等等,谁知道接下来会发生什么。

尽管这是可以理解的——程序员根据他们的职业在不同的环境下工作。对于那些在相对较大的公司里工作的人来说,他们的部门以舒适的方式处理不同的、定义明确的软件/应用程序,没有即将到来的预算削减/裁员的厄运,也没有必要以廉价/快速/可靠的方式处理大量不同的东西,单身似乎没有那么必要。甚至可能妨碍他们已经拥有的东西。

但是对于那些需要在“敏捷”开发的肮脏战壕中工作的人来说,必须实现来自他们的客户/经理/项目的许多不同的请求(有时是不合理的) ,由于前面已经解释过的原因,单例模式是一种可取之处。

另一个反对意见是它的内存占用更高

因为每个客户端的每个请求都将存在一个新的单例,所以这可能是 PHP 的一个异议。由于构造和使用的单例程序都很糟糕,如果应用程序在任何给定的位置为许多用户提供服务,那么应用程序的内存占用可能会更高。

尽管如此,这对于您在编写代码时可以采取的任何方法都是有效的。应该问的问题是,这些单例模块所持有和处理的方法和数据是否是不必要的?因为,如果它们在许多应用程序的请求中是必需的,那么即使你不使用单例,这些方法和数据也会通过代码以某种形式出现在你的应用程序中。所以,这就变成了一个问题,当你将传统类对象1/3初始化到代码处理中,并将其3/4破坏到代码处理中时,你将节省多少内存。

看,当这样说的时候,问题变得相当无关紧要-不应该有不必要的方法,数据保存在对象在您的代码中的任何方式-无论您使用或不使用单例。因此,这个对单例的反对变得非常滑稽,它假设在你使用的类中创建的对象中会有不必要的方法和数据。

- 一些无效的反对意见,如“使维护多个数据库连接不可能/更难”

我甚至不能开始理解这个异议,当一个人需要维护多个数据库连接,多个数据库选择,多个数据库查询,多个结果集在一个给定的单例只是把它们保持在变量/数组中,只要它们是需要的。这可以很简单地将它们保存在数组中,但是您可以发明任何您想要使用的方法来实现这一点。但是让我们来看看最简单的情况,在给定的单例中使用变量和数组:

假设下面的代码位于一个给定的数据库单例模式中:

$this -> Connection = 数组() ; (错误的语法,我只是像这样键入它来给你图片-变量的正确声明是 public $Connection = array () ; 它的用法自然是 $this-> Connection [‘ Connectionkey’])

您可以以这种方式在一个数组中设置并在任何给定时间保持多个连接。查询、结果集等也是如此。

$this -> query (QUERYSTRING,‘ queryname’,$this-> Connections [‘ spectrconnect’]) ;

它可以只查询具有选定连接的选定数据库,并只存储在

$this -> 结果

键为“ queryname”的数组。当然,您需要为此编写查询方法代码-这是很容易做到的。

这使您能够根据需要维护实际上无限数量的(当然是资源限制允许的数量)不同的数据库连接和结果集。并且它们可用于任何给定代码库中任何给定点上的任何代码片段,这个单例类已经被实例化到该代码库中。

当然,您自然需要释放结果集,并在不需要的时候释放连接——但这是不言而喻的,而且它并不特定于单例或任何其他编码方法/样式/概念。

此时,您可以看到如何在同一个单例中维护到第三方应用程序或服务的多个连接/状态。没什么不同。

长话短说,最后,单例模式只是另一种方法/样式/编程哲学,当它们在正确的地方以正确的方式使用时,它们和其他任何模式一样有用。这和其他事情没什么区别。

您会注意到,在大多数抨击单例的文章中,您还会看到提到“全局变量”是“邪恶的”。

让我们面对它-任何不正确使用,滥用,滥用,是邪恶的。这并不局限于任何语言,任何编码概念,任何方法。每当你看到有人发表诸如“ X 是邪恶的”这样的笼统陈述时,赶紧离开那篇文章。它很有可能是一个有限观点的产物——即使这个观点是某个特定领域多年经验的结果——这通常最终是在特定风格/方法下工作过度的结果——典型的智力保守主义。

可以给出无数的例子,从“全局变量是邪恶的”到“ iframe 是邪恶的”。大约10年前,甚至提议在任何给定的应用程序中使用 iframe 都是异端邪说。然后是 Facebook,到处都是 iframe,看看发生了什么—— iframe 不再那么邪恶了。

仍然有人固执地坚持认为他们是“邪恶的”——有时候也是出于好的理由——但是,正如你所看到的,这是一种需求,iframe 填补了这种需求,并且工作得很好,因此整个世界就继续前进。

程序员/编码员/软件工程师最重要的资产是一个自由、开放和灵活的头脑