在PHP中,单例模式与数据库访问有用场景吗?

142

我通过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中是否有单例的使用案例我忽略了?


这个相关问题中有很多资源和讨论: '单例模式有哪些缺点?' - FruitBreak
你最后的代码示例添加了一个隐藏的依赖项,这被认为是不好的实践。更好的方法是将数据库连接作为方法参数注入,因为这样无论用户查看函数还是其文档,每个使用它的人都知道该函数使用数据库类的实例,如果省略,则会自动注入。 此外,在我谦虚的意见中,该函数应该像这样: function some_function($db = Database::get()){$db::query('...');} - Alexander Behling
此外,我倾向于将函数get重命名为getInstance,因为这个命名一方面更具描述性和更为人所知(大多数Singleton示例都使用它),另一方面也不再存在与魔术方法__get混淆的危险。 - Alexander Behling
11个回答

324

单例在PHP中几乎没有用处,如果不是完全没有的话。

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

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

另一个目的是在同一请求中拥有对实例的全局访问点。虽然这听起来很理想,但它确实不是这样,因为它会创建与全局范围(如任何全局和静态变量)的耦合。这使得单元测试更加困难,而且您的应用程序总体上也不易维护。虽然有减轻这种情况的方法,但通常情况下,如果您需要在许多类中拥有相同的实例,请使用依赖注入

请查看我的幻灯片,了解有关PHP中单例模式的问题以及如何从应用程序中消除它们的原因

即使是单例模式的发明者之一Erich Gamma现在也对这种模式表示怀疑:

“我支持放弃Singleton。它的使用几乎总是一种设计上的瑕疵”

进一步阅读

如果在上述内容之后,您仍需要帮助做决定:

Singleton Decision Diagram


1
@Gordon 是的。即使在请求之间保持对象是可能的,单例模式仍会违反几个SOLID原则并引入全局状态。 - Gordon
4
很抱歉与潮流背道而驰,但 DI 并不是解决 Singleton 所用的问题的真正解决方案,除非你满足于使用具有 42 个构造参数的类(或需要 42 次 setFoo() 和 setBar() 调用才能使其正常工作)。是的,一些应用程序不幸地必须保持这种耦合并依赖于许多外部因素。PHP 是一种黏合语言,有时需要将许多东西粘在一起。 - StasM
14
如果你需要42个构造函数参数或者需要很多的setter方法,那么你就做错了。请观看《Clean Code Talks》。抱歉,我不能再次解释了。如需更多信息,请在PHP聊天室中咨询。 - Gordon

83

好的,我在开始我的职业生涯时曾经思考过这个问题。我以不同的方式实现了它,并得出了两个选择不使用静态类的原因,但它们都是相当重要的。

首先,你会发现有些东西你绝对确定永远不会超过一个实例,但最终可能会有第二个。你可能会有第二个监视器、第二个数据库、第二个服务器等等。如果你使用静态类,当这种情况发生时,你需要进行比使用单例更糟糕的重构。单例本身就是一种稀奇古怪的模式,但它可以相对容易地转换为智能工厂模式,甚至可以转换为使用依赖注入而不需要太多麻烦的代码。例如,如果通过getInstance()获取单例,你可以很容易地将其更改为getInstance(databaseName),从而允许多个数据库,而无需进行其他代码更改。

第二个问题是测试(其实这和第一个问题是相同的)。有时你想用模拟数据库来替换你的数据库。实际上,这是数据库对象的第二个实例。使用静态类比使用单例更难实现这一点,你只需要模拟getInstance()方法,而不是每个静态类中的所有方法(在某些语言中可能非常困难)。

这真的取决于习惯——当人们说“全局变量”很糟糕时,他们有很好的理由这样说,但在你自己遇到这个问题之前,这可能并不总是显而易见的。

你能做的最好的事情就是像你刚才所做的那样询问,并做出选择,观察你的决定对代码演变产生的影响。具备解释代码演变知识比一开始就完美地完成更加重要。


15
你说单例可以很好地转换为依赖注入,但是你的getInstance(databaseName)示例是否仍然只是将对全局实例库的引用散布在代码中?调用getInstance的代码应该由客户端代码注入实例(或实例集),因此不需要首先调用getInstance - Will Vousden
1
@Will Vousden 正确,这有点像一个权宜之计。它并不是真正的 DI,但它可以非常接近。例如,如果它是 getInstance(supportedDatabase),并且返回的实例是基于传入的数据库计算出来的呢?重点是要避免在人们准备好使用 DI 框架之前吓到他们。 - Bill K

23

谁需要在PHP中使用单例模式?

注意到几乎所有对单例模式的反对都来自技术角度 - 但它们在范围上也非常有限,特别是对于PHP。首先,我将列出一些使用单例模式的原因,然后分析使用单例模式的反对意见。首先,需要它们的人:

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

你看,单例模式是自包含的。当完成后,一个单例类在任何引用它的代码中都是不可改变的,其行为完全符合你创建方法和变量的方式。而且在给定的请求中始终是相同的对象。由于不能创建两个不同的对象,因此你可以在任何代码中的任何给定点上了解单例对象 - 即使将单例插入到两个、三个不同的旧蜘蛛网代码库中。因此,在开发目的上更容易 - 即使有许多人在该项目中工作,当你看到单例在任何代码库中的某个点被初始化时,你知道它是什么,它做什么,如何做以及它所处的状态。如果它是传统的类,你需要跟踪对象最初创建的位置,在代码中调用了哪些方法以及它特定的状态。但是,把一个单例放在那里,如果你在编写代码时加入了适当的调试和信息方法和跟踪,你就知道它是什么。因此,它使那些必须与不同代码库一起工作的人更容易,其需要将先前用不同哲学或由你没有联系的人完成的代码集成进去。(也就是说,供应商-项目-公司或其他已不再提供支持的情况)。

- 需要使用第三方APIs、服务和网站的人。

如果你仔细看,这与早期情况并没有太大区别 - 第三方API、服务、网站就像是你无法控制的外部独立代码库。任何事情都可能发生。因此,通过单例会话/用户类,您可以管理来自第三方提供商(如OpenIDFacebookTwitter等)的任何类型的会话/授权实现,并且您可以同时从同一个单例对象中进行所有操作 - 这个对象很容易访问,在任何时候都处于已知状态,无论您将其插入到哪个代码中。您甚至可以为您自己的网站/应用程序中的同一用户创建多个会话以连接到多个不同的第三方API/服务,并对它们执行任何您想要的操作。

当然,所有这些也可以使用普通类和对象的传统方法来完成 - 这里的问题是,单例更加整洁、简洁,因此在这种情况下与传统的类/对象使用相比更易于管理/测试。

- 需要进行快速开发的人

单例的全局行为使得使用具有一组单例构建的框架构建任何类型的代码变得更加容易,因为一旦你很好地构建了你的单例类,已经建立、成熟和设置的方法将随时随地以一致的方式轻松可用。虽然需要一些时间来成熟你的类,但之后它们就会非常稳定、一致和有用。你可以在一个单例中拥有许多方法来做任何你想做的事情,尽管这可能会增加对象的内存占用,但它带来的节省时间远远超过了这个代价 - 你在一个应用程序实例中没有使用的方法可以在另一个集成的实例中使用,并且你只需要进行一些修改就可以添加客户/老板/项目经理要求的新功能。

你明白了吧。现在让我们继续讨论对单例的反对意见和反对某些有用东西的不可思议的追求

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

实际上,单例模式在某种程度上确实存在问题,即使通过采取适当的预防措施和将调试程序编码到单例中来缓解这些问题,同时也要认识到您将需要调试单例。但是请注意,这与任何其他编码哲学/方法/模式并没有太大区别 - 只是单例相对较新且不普及,因此当前的测试方法与它们相比较不兼容。但这在编程语言的任何方面都没有什么不同 - 不同的风格需要不同的方法。

这个反对意见的一个问题在于,它忽略了应用程序开发的原因不是为了“测试”,而测试也不是应用程序开发中唯一的阶段/过程。应用程序是为生产使用而开发的。正如我在“谁需要单例”部分所解释的那样,单例可以从必须使代码在许多不同的代码库/应用程序/第三方服务中工作的复杂性中削减很多。在测试中可能会浪费的时间,在开发和部署中获得的时间更多。这在第三方身份验证/应用程序/集成的时代尤其有用 - Facebook、Twitter、OpenID、更多的服务以及未来的服务。

尽管可以理解 - 程序员的工作环境因其职业而异。对于那些在相对大型公司中工作,有明确定义的部门管理不同的软件/应用程序,并且在舒适的方式下没有预算削减/裁员的迫在眉睫和伴随而来的需要以便宜/快速/可靠的方式完成许多不同的任务,单例可能看起来并不是必要的。甚至可能会妨碍他们已经拥有的东西。

但是对于那些需要在“敏捷”开发的肮脏战壕中工作,需要实现客户/经理/项目提出的许多不同请求(有时是不合理的),由于前面所述的原因,单例是一种拯救之道。

- 另一个反对意见是其内存占用较高

因为每个客户端的每个请求都将存在一个新的单例,这可能是PHP的一个反对意见。如果单例构造和使用不当,则应用程序的内存占用量可能会更高,如果在任何给定点为许多用户提供服务,则情况更加如此。

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

如此说来,这个问题就变得非常无关紧要 - 在您的代码中,无论您使用单例模式与否,都不应该存在不必要的方法和数据 - 这是不言自明的。因此,对单例模式的反对变得真正滑稽可笑,因为它假定您使用的类所创建的对象中将存在不必要的方法和数据。

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

我甚至无法理解这个反对意见,因为只需要将多个数据库连接、多个数据库选择、多个数据库查询、多个结果集保持在单例中的变量/数组中,只要它们需要就可以了。这可以简单地通过将它们保存在数组中来实现,尽管您可以发明任何您想要使用的方法来实现它。但让我们来看看最简单的情况,在给定的单例中使用变量和数组:

想象下面的内容在一个给定的数据库单例中:

$this->connections = array(); (错误的语法,我只是这样打字来给你一个画面 - 变量的正确声明方式是public $connections = array();,它的用法是$this->connections['connectionkey']自然)

您可以以这种方式设置和保持多个连接。同样适用于查询、结果集等。

$this->query(QUERYSTRING,'queryname',$this->connections['particulrconnection']);

这只是使用所选连接对所选数据库进行查询,并将其存储在您的

$this->results

数组中的键名为“queryname”。当然,您需要编写查询方法来实现它 - 这很容易。

这使您能够维护几乎无限数量的(当然,只要资源限制允许)不同的数据库连接和结果集,尽可能多地使用它们。并且它们对于任何代码片段都是可用的,在任何给定代码库中被实例化的单例类中的任何给定点。

当然,您自然需要在不需要时释放结果集和连接 - 但这是不言而喻的,并且它与单例或任何其他编码方法/风格/概念无关。

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

长话短说,最终,单例模式只是另一种编程方法/风格/哲学,当它们在正确的位置以正确的方式使用时,它们就像任何其他有用的东西一样。这与其他任何事物没有什么不同。

您会注意到,在大多数批评单例的文章中,您还会看到对“全局变量”被视为“邪恶”的引用。

让我们面对现实吧——任何没有得到适当使用、被滥用和误用的东西都是邪恶的。这不限于任何语言、编码概念或方法。每当你看到有人发布类似“X是邪恶的”这样的笼统声明时,就要远离该文章。很有可能这是一种狭隘的观点产生的结果,即使这个观点是特定领域多年经验的结果,也通常是某种给定风格/方法下工作过度的结果——典型的知识保守主义。

可以举无数例子来说明这一点,从“全局变量是邪恶的”到“iframe是邪恶的”。大约十年前,甚至在任何一个应用程序中建议使用iframe都是异端邪说。然后Facebook出现了,到处都是iframe,结果怎么样呢?iframe不再那么邪恶了。

仍然有人执拗地坚称它们是“邪恶的”,有时也有充分的理由,但正如你所看到的,有需求,iframe能满足这种需求并且运作良好,因此整个世界就这样继续前进。

程序员/编码人员/软件工程师的最重要资产是一种自由、开放和灵活的思维方式。


3
虽然我同意拥有开放和灵活的思维对于任何开发者来说都是必备的资产,但这并不能使单例模式成为反模式。上述答案包含了许多关于单例模式本质和影响的不准确陈述和错误结论,因此我只能将其投下反对票。 - Gordon
我不得不亲身体验了一个有许多单例的框架,而且自动测试是不可能的。我必须通过在浏览器中反复试错来手动测试所有内容。一些错误可以通过代码审查(拼写、语法错误)来避免,但功能性错误通常是隐藏的。这种测试需要比单元测试更多的时间。通过单元测试,我可以说:这个类在隔离状态下工作,错误一定是出现在其他地方。没有调试是很繁琐的。 - Jim Martens
该框架必须具备内置的日志记录和错误跟踪功能。此外,一个在隔离状态下正常工作的类,在放入更广泛的应用程序中以单例形式运行时也应该正常工作。这意味着,在这种情况下,导致问题的将是与该单例交互的另一个类或函数。这与大型应用程序内部的普通错误跟踪没有什么不同。而要进行有效的错误跟踪,则需要应用程序具备适当的日志记录功能。 - unity100
不准确。大量的单例绝对是有害的,因为它会导致测试地狱。 :-) 然而,每个应用程序只有一个单例可能是好的。例如:作为统一的日志记录功能 - 在所有应用程序中实现(包括一些遗留代码)。 - Filip OvertoneSinger Rydlo
“在测试中可能会浪费的时间…”这是一种非常糟糕的做法和思维方式。所有那些遗留的应用程序都是以此为前提开发的,因此它们变得难以维护,需要重新编写。如果没有测试,当开发新功能并在系统的其他部分中出现故障时,将会浪费时间。调试所花费的时间,用户无法正确使用该功能所花费的时间,应用程序的信心等都会丧失。 - bogdancep

15
Singletons被许多人认为是反模式,因为它们只是经过美化的全局变量。在实践中,很少有情况需要一个类仅有一个实例;通常只需要一个实例就足够了,在这种情况下,将其实现为单例完全是不必要的。
回答这个问题,你是正确的,单例在这里是过度设计。一个简单的变量或函数就可以胜任。然而,更好(更健壮)的方法是使用依赖注入来消除对全局变量的需求。

但是单例模式可以很顺利地转换为依赖注入,而静态类则不能,这就是静态类的真正问题所在。 - Bill K
@Bill:非常正确,这就是为什么我会提倡一开始就采用 DI 方法,而不是松散的函数或静态方法 :) - Will Vousden
在某些语言中(如Java),静态类(或类的静态方法)无法被扩展。因此,您为未来的开发人员创建了潜在的问题(或者说更多的工作)。因此,一些人建议除非您有特定的需求,否则应该通常避免使用静态方法。 - Marvo

8
在您的示例中,您正在处理一个看似不变的单个信息。对于这个示例,使用Singleton模式会过度设计,只需在类中使用静态函数即可。
更多想法:您可能正在经历一种为了模式而实现模式的情况,您的直觉告诉您“不需要”,原因就像您所述。
但是:我们不知道您的项目规模和范围。如果这是简单的代码,也许是丢弃的代码,不太可能需要更改,那么是的,可以使用静态成员。但是,如果您认为您的项目可能需要扩展或准备进行维护编码,则可能需要使用Singleton模式。

1
哇,完全错了。区别(问题的答案)的整个重点在于,如果你使用静态方法,以后修复代码以添加第二个实例会更加困难。如果你使用静态方法,这样说就像是在说“在你的有限条件下,全局变量没问题”,而全局变量的整个问题就在于条件会发生变化。 - Bill K
@Bill K:我同意你的观点,如果有任何复杂性,我会使用单例模式。但是我试图从OP的角度回答问题,并认为,在这种非常有限的情况下,是的,我想这确实是过度设计了。当然,我忽略了架构或可扩展性方面以及大量其他考虑因素。我是否应该在我的答案中包含这样一个警告,同时解释为什么某人应该始终使用单例模式...这肯定会引起其他人的反对? - Paul Sasik

5

首先,我想说的是我并不认为单例模式有太多用处。为什么要在整个应用程序中保持一个对象?特别是对于数据库,如果我想连接到另一个数据库服务器怎么办?我每次都必须断开和重新连接吗?无论如何...

在应用程序中使用全局变量(这是传统单例模式的用法)存在几个缺点:

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

使用静态类代替单例实例也会带来一些相同的缺点,因为单例最大的问题是静态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模式),让它们使用。

5
考虑你的解决方案与PHP文档中所提供的解决方案有何不同。实际上,只有一个“小”差别:你的解决方案为getter的调用者提供了PDO实例,而文档中的解决方案为Database::singleton的调用者提供了Database实例(然后使用该实例上的getter获取PDO实例)。
那么我们得出什么结论呢?
- 在文档代码中,调用者会获得一个Database实例。Database类可能会公开(实际上,如果您要做所有这些工作,则应该公开)比其包装的PDO对象更丰富或更高级的接口。 - 如果您将实现更改为返回比PDO更丰富的另一种类型,则两个实现是等效的。从遵循手动实现中无法获得任何收益。
在实践中,单例模式是一个相当有争议的模式。这主要是因为:
- 它被过度使用。新手程序员比其他模式更容易理解单例模式。他们随后会在任何地方应用他们的新发现,即使手头的问题可以更好地无需使用单例模式解决(当你拿着一把榔头时,所有东西都看起来像钉子)。 - 根据编程语言的不同,在无漏洞、非泄露方式下实现单例模式可能是一项巨大任务(特别是如果我们有高级场景:一个依赖于另一个单例模式的单例模式,可以销毁和重新创建的单例模式等)。试着搜索C++中“定义性”的单例模式实现,我敢打赌你做不到(我拥有Andrei Alexandrescu开创性的《现代C++设计》这本书,其中记录了大部分混乱)。 - 它在编写单例模式代码和访问它的代码时增加了额外的工作量,您可以通过对尝试使用程序变量做什么施加一些自我约束来避免。
因此,最终结论是:您的单例模式是完全可以的。大多数情况下,根本不使用单例模式也是可以的。

2

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


2
你的理解是正确的。单例在某些情况下很有用,但常常被过度使用。通常情况下,访问静态成员函数就足够了(特别是当你不需要以任何方式控制构建时间时)。更好的做法是将一些自由函数和变量放到命名空间中。

2
编程并没有所谓的“对”或“错”,只有“好的实践”和“不好的实践”。
单例通常被创建为一个类,以便以后重复使用。它们需要以这样的方式创建,以便程序员在午夜醉酒编码时不会意外地实例化两个实例。
如果您有一个简单的小类,不应该被实例化多次,那么您不需要将其作为单例。如果您这样做,它只是一个安全网。
并不总是坏实践拥有全局对象。如果您知道将在全局/任何地方/所有时间中使用它,则可能是为数不多的几个例外之一。然而,全局变量通常被认为是“不好的实践”,就像goto一样。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接