是否存在用PHP访问数据库的单例的用例?


138

我通过PDO访问我的MySQL数据库。我正在设置对数据库的访问权限,而我的第一个尝试是使用以下内容:

我想到的第一件事是global

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

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

这被认为是不好的做法。稍作搜索后,我得到了Singleton模式,该模式

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

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

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 2012年

Answers:


80

好的,当我刚开始我的职业生涯时,我想了一下。以不同的方式实现它,并提出了两个选择不使用静态类的理由,但它们是很大的理由。

一种是您会发现很多情况下,您绝对可以确定自己永远不会有一个以上的实例,而最终又会有一个实例。您可能最终会得到第二个监视器,第二个数据库,第二个服务器(无论如何)。

发生这种情况时,如果您使用静态类,则与使用单例相比,您的重构困难得多。单例本身就是一个不稳定的模式,但是它很容易转换为智能工厂模式-甚至可以转换为使用依赖注入而不会带来太多麻烦。例如,如果您的单例是通过getInstance()获取的,则可以轻松地将其更改为getInstance(databaseName)并允许多个数据库-无需更改其他代码。

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

这实际上归结为习惯-当人们说“全球”很糟糕时,他们有很好的理由这么说,但是在您亲自解决问题之前,它可能并不总是显而易见的。

您可以做的最好的事情是(像您一样)询问,然后做出选择并观察决定的后果。与一开始就拥有正确的知识相比,拥有了解随着时间的推移代码演变的知识更为重要。


15
您说单例会很好地降级为DI,但是您的示例是否getInstance(databaseName)还仅仅是在整个代码中分散对实例的全局存储库的引用?将会调用的代码getInstance应具有由客户端代码插入的实例,因此不需要首先调用getInstance
Will Vousden

1
@Will Vousden正确,这是一个权宜之计。它不是真正的DI,但可以非常接近。例如,如果它是getInstance(supportedDatabase),并且返回的实例是根据传入的数据库来计算的,该怎么办?关键是要避免在使用DI框架之前就吓people他们。
比尔K

320

单例几乎没有-不用说-不可以在PHP中使用。

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

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

另一个目的将是拥有对同一请求内实例的全局访问点。尽管这听起来可能是合乎需要的,但实际上并非如此,因为它会创建与全局范围的耦合(如所有全局变量和静态变量)。这使得单元测试更加困难,并且您的应用程序通常难以维护。有多种方法可以减轻这种情况,但是通常,如果您需要在多个类中使用同一实例,请使用Dependency Injection

请参阅我的PHP单例幻灯片-为什么它们不好,以及如何从应用程序中删除它们以获取更多信息。

甚至是Singleton模式的发明者之一Erich Gamma如今都对这种模式表示怀疑:

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

进一步阅读

如果在上述之后仍需要帮助来确定:

单例决策图


1
@戈登是的。即使可以在请求之间维护对象,但Singleton仍然违反了SOLID几项原则,并引入了Global State。
Gordon

4
抱歉,与流程相反,但是DI并不是解决Singleton所用问题的解决方案,除非您对具有42个ctor参数的类(或使其具有42个setFoo()和setBar()调用的类感到满意)工作)。是的,不幸的是,某些应用程序确实必须结合在一起,并且确实依赖于许多外部因素。PHP是一种粘合语言,有时会有很多东西需要粘合在一起。
StasM,2012年

14
@StasM如果您有42个ctor参数或需要大量的设置器,那么您做错了。请观看“干净代码讨论”。抱歉,如果我不愿意再解释一次。欢迎在PHP聊天室中询问更多信息。
戈登

@戈登php聊天室在哪里?
user658182

21

谁需要PHP中的单例?

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

-编码大型框架/代码库(将在许多不同的环境中使用)的人们将不得不使用以前存在的不同框架/代码库,必须实现来自客户端/老板的许多不同,变化甚至是异想天开的请求/管理/部门负责人。

看到,单例模式是自我包容的。完成后,单例类对于包含在其中的任何代码都是严格的,并且其行为与您创建其方法和变量的方式完全相同。在给定的请求中,它始终是同一对象。由于不能两次将其创建为两个不同的对象,因此您知道代码中任何给定点的单例对象-即使将单例插入到两个,三个不同的旧的甚至是意大利面条式的代码库中。因此,就开发目的而言,它使工作变得更容易-即使在该项目中工作的人很多,当您看到某个给定代码库中的某个点在某个点中初始化了一个单例时,您也知道它是什么,它做什么,如何做。以及它所处的状态。如果它是传统类,则需要跟踪该对象最初在何处创建,直到代码中的这一点为止,在其中调用了哪些方法及其特定状态。但是,在此放置一个单例,如果您在编码时放弃了适当的调试和信息方法并跟踪到该单例,则您确切地知道它是什么。因此,这使得必须使用不同代码库的人变得更容易,并且有必要集成以前用不同的方法完成的代码或由您没有接触过的人完成的代码。(也就是说,vendor-project-company-无论什么都没有,什么都不支持)。对于必须使用不同代码库的人们来说,它变得更容易,并且有必要集成以前用不同的哲学完成的代码或由您没有接触过的人们完成的代码。(也就是说,vendor-project-company-无论什么都没有,什么都不支持)。对于必须使用不同代码库的人们来说,它变得更容易,并且有必要集成以前用不同的哲学完成的代码或由您没有接触过的人们完成的代码。(也就是说,vendor-project-company-无论什么都没有,什么都不支持)。

-需要使用第三方API,服务和网站的人员。

如果您仔细观察,这与先前的情况并没有太大不同-第三方API,服务,网站就像外部不受控制的独立代码库。什么都可能发生。因此,使用单例会话/用户类,您可以管理来自第三方提供商(如OpenIDFacebookTwitter等)的任何类型的会话/授权实施-并且您可以从SAME单例对象同时完成所有这些操作-在任何状态下,无论插入哪种代码,都可以在已知状态下轻松访问它。您甚至可以在自己的网站/应用程序中为SAME用户创建到多个不同的第三方API /服务的多个会话,并执行与它们相关的任何操作。

当然,通过使用正常的类和对象,所有这些也可以与传统方法配合使用-这里的问题是,单例更加整齐,整洁,因此,与此类情况下的传统类/对象用法相比,这种方法易于管理/可测试。

-需要快速发展的人

单例的类全局行为使使用带有可构建单例集合的框架构建任何类型的代码变得更加容易,因为一旦构建好单例类,就可以轻松地使用已建立,成熟和设置的方法,并且可以随时随地以一致的方式使用。使您的课程成熟需要一些时间,但是在那之后,它们是坚如磐石,一致且有用的。您可以在一个单例中拥有任意数量的方法来完成您想要的任何事情,尽管这可能会增加对象的内存占用量,但它可以节省更多的时间来快速开发-一种在给定实例中不使用的方法一个应用程序可以用在另一个集成的应用程序中,而您只需通过一些修改即可获得客户/老板/项目经理要求的一项新功能。

你明白了。现在,让我们继续对单身人士的反对,以及反对有用的东西的邪恶斗争

-最主要的异议是,这会使测试更加困难。

实际上,即使在采取适当的预防措施并将调试例程编码为单例代码的情况下可以轻松缓解该问题,并且意识到您将要调试单例代码的情况下,它确实在一定程度上可以做到。但是请注意,这与现有的任何其他编码原理/方法/模式都没有太大不同-只是,单例是相对较新的并且并不广泛,因此当前的测试方法最终与它们不兼容。但这在编程语言的任何方面都没有什么不同-不同的样式需要不同的方法。

这一反对意见的一个观点是,它忽略了以下事实:开发的应用程序不是用于“测试”的原因,并且测试不是进入应用程序开发的唯一阶段/过程。开发了用于生产的应用程序。正如我在“谁需要单例”一节中所解释的那样,单例可以使代码必须与之配合使用并在许多不同的代码库/应用程序/第三方服务中使用,从而减少了很多交易。在测试中可能浪费的时间,是在开发和部署中获得的时间。在第三方身份验证/应用程序/集成的时代,这特别有用-Facebook,Twitter,OpenID,还有更多,而且谁知道下一步是什么。

尽管这是可以理解的,但程序员的职业取决于他们在不同情况下的工作。对于在相对较大的公司中工作的人员,这些部门的定义部门以舒适的方式管理不同的,定义的软件/应用程序,而没有即将削减预算/裁员的厄运,并且随之而来的是需要做很多事情,其中​​包含很多不同的东西一种便宜/快速/可靠的方式,单身人士似乎没有必要。甚至可能妨碍了他们已经拥有的东西。

但是对于那些需要在“敏捷”开发的肮脏战work中工作,不得不执行来自其客户/经理/项目的许多不同请求(有时是不合理的)的人来说,由于前面解释的原因,单例是一种节省的选择。

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

因为对于来自每个客户端的每个请求都会存在一个新的单例,所以对于PHP可能是一个反对。如果单例构造和使用不正确,则在任何给定时间点,如果应用程序为许多用户提供服务,则应用程序的内存占用量可能会更高。

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

瞧,用这种方式说来,这个问题就变得无关紧要了-不管有没有使用单例,都不应有不必要的方法,无论如何代码都保存在代码中的对象中。因此,对单例的反对变得非常有趣,因为它假设将有不必要的方法,即从您使用的类创建的对象中的数据。

-一些无效的异议,例如“使维护多个数据库连接变得不可能/困难”

我什至无法理解这个异议,当所有人都需要维护多个数据库连接,多个数据库选择,多个数据库查询,给定单例中的多个结果集时,只要将它们保留在单例中的变量/数组中,只要他们是必要的。这可以像将它们保存在数组中一样简单,尽管您可以发明任何想要使用的方法来实现。但是,让我们研究最简单的情况,在给定的单例中使用变量和数组:

假设以下内容位于给定的数据库单例中:

$ this- > connections = array(); (错误的语法,我只是这样输入以便给您显示图片-变量的正确声明是public $ connections = array();它的用法自然是$ this-> connections ['connectionkey'])

您可以通过这种方式在阵列中的任何给定时间建立并保留多个连接。对于查询,结果集等也是如此。

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

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

$ this- >结果

键为“ queryname”的数组。当然,您需要为此编写查询方法-这很简单。

这使您可以维护几乎无限数量(当然,资源限制所允许的最大数量)的不同数据库连接和结果集,所需数量不限。它们可用于实例化此单例类的任何给定代码库中任何给定点中的任何代码段。

当然,您自然需要释放结果集和不需要的连接-但这不用说,并且它不单例或任何其他编码方法/样式/概念。

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

长话短说,最后,单例模式只是用于编程的另一种方法/样式/理念,当以正确的方式在正确的位置使用它们时,它们与任何其他模式一样有用。这没有什么不同。

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

让我们面对现实-任何未正确使用,滥用,误用的东西都是邪恶的。那不限于任何语言,任何编码概念,任何方法。每当您看到有人发布诸如“ X是邪恶的”之类的笼统声明时,就不要去看那篇文章。很有可能这是有限的观点的产物-即使观点是多年特定经验的结果-通常最终是由于以给定的风格/方法工作过多而导致的-典型的知识保守主义。

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

仍然有些人顽固地坚持自己是“邪恶的”-有时也是出于充分的理由-但是,正如您所看到的,有一种需求,iframe可以满足这种需求并且运作良好,因此整个世界都在前进。

程序员/编码器/软件工程师的首要资产是自由,开放和灵活的思想。


2
-1。尽管我同意开放和灵活的思维是任何开发人员的必备条件,但它并不能代替Singleton成为反模式。上面的答案包含许多关于Singleton的性质和影响的不准确陈述和错误结论,我不得不对此予以否决。
Gordon

-1。我必须亲身体验具有很多单例的框架,并且不可能进行自动测试。我必须通过浏览器中的反复试验来手动测试所有内容。使用代码审阅可以避免某些错误(拼写,语法错误),但是功能错误通常被隐藏。与单元测试相比,此测试需要更多的时间。对于单元测试,我可以说:此类是独立工作的,错误必须在其他地方。没有调试就很乏味。
Jim Martens 2014年

该框架必须内置日志记录和错误跟踪。同样,一个类在隔离状态下正常工作,当将其放入更广泛的应用程序中时,也可以单例形式正常工作。这意味着在这种情况下,正在中断的将是与该单例交互的另一个类或函数。这与大型应用程序中的常规错误跟踪没有什么不同。如果应用程序没有正确的日志记录,这本身就很难。
unity100

不准确 大量的单调绝对是邪恶的,因为它会产生Testing-HELL。:-)但是,每个应用一个单音可能很好。例如:作为统一的日志记录功能-跨所有应用程序(包括一些旧代码的应用程序)实施。
Filip Overtone歌手Rydlo,2016年

“测试中可能会浪费的时间……”这是一种非常糟糕的做法和思维方式。考虑到所有这些遗留应用程序都是出于这种考虑而开发的,因此无法对其进行维护,因此需要对其进行重写。如果没有测试,则开发新功能并破坏系统其他部分功能的时间将会浪费。在调试上浪费的时间,用户可以正确使用该功能的时间,对应用程序丢失的信心等
。– bogdancep

15

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

要回答这个问题,您是对的,单身人士在这里过分正确了。一个简单的变量或函数即可。但是,更好(更健壮)的方法是使用依赖注入来完全消除对全局变量的需求。


但是Singletons可以非常平滑地降级为DI,而静态类则不能,这是静态类的真正问题。
比尔K

@Bill:非常正确,但这就是为什么我主张采用DI方法而不是松散的函数或静态方法开始的原因:)
Will Vousden

在某些语言(例如Java)中,不能扩展静态类(或类的静态方法)。因此,您为将来的开发人员带来了潜在的问题(或充其量是更多的工作)。因此,有人建议通常应避免使用静态方法,除非您有特殊需要。
Marvo

8

在您的示例中,您正在处理的是一条看似不变的信息。对于此示例,Singleton可能会过大,仅在类中使用静态函数就可以了。

更多的想法:您可能正在为实现模式而实施模式的案例,而您的直觉告诉您“不,您不必”,因为您已阐明了原因。

但是:我们不知道您的项目的规模和范围。如果这是简单的代码(可能会丢弃),则无需更改,那么可以,继续使用静态成员。但是,如果您认为您的项目可能需要扩展或为将来的维护编码做好准备,那么可以,您可能想要使用Singleton模式。


1
哇,简直是错。区别的全部要点(问题的答案)是,以后修复代码以添加第二个实例要困难得多。如果您使用静态方法,则很难做到这一点。当Globals的整个问题是条件改变时,这就像在说“ Globals在有限的条件下很好”。
Bill K

@Bill K:我同意你的看法,如果有任何复杂性,我会使用单例。但是我试图从OP的观点和思想来回答这个问题,是的,我想在这种非常有限的情况下,这是过分的。当然,我忽略了体系结构或可伸缩性方面的考虑以及其他许多考虑因素。我是否应该在回答中加入这一点作为警告,然后解释为什么有人应该始终使用单身人士……这肯定会引起他人的否决?
Paul Sasik 2011年

5

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

在应用程序中使用全局变量有几个缺点(这是Singleton模式的传统用法所做的事情):

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

使用静态类代替单例实例也具有一些相同的缺点,因为单例的最大问题是静态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实例,而docs中的解决方案为getter的调用者提供Database::singleton了一个Database实例(然后在该PDO实例上使用getter来获取实例)。

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

  • 在文档代码中,调用者获得一个Database实例。该Database班可暴露(事实上,它应该,如果你“重新去一切麻烦揭露),比一个富裕或更高级别的接口PDO它包装的对象。
  • 如果将实现更改为返回另一个(更丰富的)类型PDO,则这两个实现是等效的。遵循手动实施没有任何收获。

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

  • 过度使用了。新手程序员比其他模式更容易掌握Singleton。然后,他们继续将他们新发现的知识应用到任何地方,即使没有Singleton也可以更好地解决当前的问题(当您拿着锤子时,一切看起来都像钉子一样)。
  • 根据编程语言,以气密,无泄漏的方式实现单例可能被证明是一项艰巨的任务(特别是如果我们有高级方案:一个单例取决于另一个单例,可以销毁并重新创建的单例,等等) )。敢于尝试(我拥有Andrei Alexandrescu突破性的Modern C ++设计,该文件记录了很多混乱情况),只需尝试在C ++中搜索“最终的” Singleton实现。
  • 在编写Singleton时和编写用于访问它的代码时,它都增加了额外的工作量,而这些工作量可以通过遵循对程序变量所做的一些自我强加的约束来完成。

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


2

您的解释是正确的。单身人士有其位置,但已被过度使用。通常,访问静态成员函数就足够了(值得注意的是,当您不需要以任何方式控制构造时间时)。更好的是,您可以将一些免费的函数和变量放在名称空间中。


2

编程时没有“对”和“错”。有“好习惯”和“坏习惯”。

通常将单例创建为一个类,以供以后重用。它们的创建方式应使程序员在午夜醉酒编码时不会意外实例化两个实例。

如果您有一个简单的小类,不应多次实例化,则无需使其成为单例。如果您这样做的话,那只是一个安全网。

拥有全局对象并不总是坏习惯。如果您知道将要在全球/各地/始终使用它,则可能是少数例外之一。但是,通常将全局变量视为“不良行为”,将其goto视为不良行为。


2

我一点都没有看到这一点。如果您以将连接字符串作为构造函数的参数并维护一个PDO对象列表(每个唯一的连接字符串一个)的方式实现该类,则可能会有一些好处,但是单例实现这种情况似乎毫无意义。


1

据我所知,您没有丢失任何东西。这个例子是有缺陷的。如果单例类具有一些非静态实例变量,那将会有所不同。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.