为什么在PHP环境中使用单例模式是不好的?

6

可能是重复问题:
谁需要单例模式?

我在想,在php脚本中使用单例模式有什么缺点。我经常使用它们,但有时无法理解开发人员的批评。以下是一些例子:

我有一个请求类:

清理POST、GET、COOKIE输入数据并在全局范围内严格使用它而不是全局数组。像这样:

$request = Request::getInstance();
$firstname = $request->post('firstname', $additionalFilters);

每个请求只有一个请求。为什么在这种情况下使用单例是一个坏主意?

对于 $_SESSION 也是一样:

我有一个 Session 类(单例),它代表了 $_SESSION 数组,因为只有一个会话并且我在全局范围内使用它。

数据库

$mysql  = DB::getInstance('mysql', 'dbname'); //pseudo
$sqlite = DB::getInstance('sqlite', 'dbname'); //pseudo

对于每种类型的数据库,我只希望有一个对象,而不是多个。我认为否则会存在混乱的风险。

唯一行

此外,我经常使用类来表示/使用数据库表的唯一行。

$article = Article::getInstance($id);
$navigation = Navigation::getInstance($id);

我认为这种做法有很多好处。我不想要一个代表唯一行的第二个对象。为什么单例在这里是一个坏主意呢?
实际上,我的大多数(几乎全部)类都没有公共构造函数,而是始终具有像getInstance($id)或create()这样的静态方法,因此类本身处理可能的实例(这并不意味着它们都是单例定义)。
所以我的问题是:还有我没有意识到的任何缺点吗?当反对单例时,单例怀疑者考虑了哪些具体情况?
编辑:
现在,您有一个封装$_POST的单例,但如果您没有$_POST,而想要使用文件作为输入怎么办?在这种情况下,如果您有一个抽象的输入类,并实例化POSTInput来通过发布的数据管理输入,则会更加方便。
好的,有有效的优点。我没有意识到这一点。特别是关于Request类的那一点。
但我仍然对这种方法持怀疑态度。假设我有一个“Functionality”类,它执行具体请求(例如留言板组件)。在该类中,我想获取发送的参数。因此,我获得了Request的单例实例。
$req = Request::getInstance(); 
$message = $req->post('message');

这种方式,只有我的功能对象关心一个请求类。当我使用非单例模式时,我需要某种额外的类/函数来管理每个请求获取有效的请求对象。这样,我的Functionality类就不需要知道关于该管理类的信息,但在我看来,仍然存在一种依赖/问题:每次创建Functionality对象的实例时,都有可能忘记设置请求对象。当然,我可以在创建Functionality时定义一个非可选参数。但总的来说,这会导致参数过载。或者不会?

变量在PHP中只存储对象的引用(除非你显式地克隆对象)。即使你像$object2 = $object;这样做,它们都将指向同一个实例。但我也喜欢单例模式 :p - Hugo Mota
2个回答

4

单例模式(和静态类,它们基本上具有相同的特点)并不是本质上的坏处,但是它会引入你可能不想要的依赖。

现在,你有一个包装 $_POST 的单例,但如果你没有 $_POST,而想使用文件作为输入呢?在这种情况下,如果你拥有一个抽象的输入类,并实例化 POSTInput 来管理通过发布的数据输入,那将更加方便。

如果你想从文件中获取输入,甚至(出于任何原因)想基于来自数据库表的输入模拟(或重放)多个请求,你仍然可以这样做,而不需要更改除实例化类的部分之外的任何代码。

其他类也适用同样的方法。你不希望你的整个应用程序都与这个 MySQL 单例通信..如果你需要连接到两个 MySQL 数据库怎么办?如果你需要切换到其他类型的 SQL 数据库呢?..为这些类制作抽象类,并重写它们以实现特定的技术。


我编辑了我的问题。它没有适合这个评论框,因此发了这条消息 :) 。无论如何,感谢你的绝妙答案。 - Johannes Reiners
我认为大多数系统不会同时连接并切换到两个数据库。如果确实需要这样做,就像你所说的那样,可以使用子类化来实现。 - Raptor

3

我认为单例模式在基于请求的架构(如PHP或ASP.NET)中并不应该像它们现在那样被批评。 在一般程序中,单例对象的生命周期可以与程序运行的时间相同,可能有数月甚至数年之久:

int main()
{
    while(dont_exit)
    {
        // do stuff
        Singleton& mySingleton = Singleton::getInstance();
        // use it
    }

    return 0;
}

除了它几乎就是全局变量之外,很难用一个在单元测试中有用的单例替换它。成百上千个源文件中可能依赖它的代码量将紧密耦合使用该单例的整个程序。
话虽如此,在基于请求的场景(例如您的PHP页面或ASP.NET页面)中,所有可调用代码实际上都被包装在函数调用中。再次说明,他们正在使用安全防护措施来混淆全局变量(但在请求上下文中)以防止多次创建。
但仍然建议不要使用它们。为什么?因为即使在您的单个请求的情况下,一切都依赖于并与该实例紧密耦合。当您想要使用不同的请求对象测试不同场景时发生了什么?假设您使用了include编码,则现在必须修改每个该调用的实例。如果您传递了对预构造请求类的引用,则现在可以执行很酷的操作,例如通过简单地更改传递给其他函数的内容提供模拟单元测试版本的类。您还将解耦使用这个通用请求对象的所有内容。

1
我知道这个问题已经关闭了,但这是一个非常好的回答!我现在明白了!非常感谢你,作为一名PHP程序员,我没有考虑到在其他保持单例模式的语言中,更改/替换它会有多么困难。此外,非常感谢您对为什么反对在单调用语言中使用它们的出色解释,我认为我将永远不会再在PHP中使用单例模式了。 - David Barker

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