常量与全局变量和单例一样邪恶


Are constants as evil as global variables and singletons?

我在这个论坛上多次听到使用全局变量是死罪,实现单例是犯罪。

我突然想到,旧的良好常数具有这些耻辱做法的所有特征:它们是全球访问的,毫无疑问,它们引入了有史以来最全球化的状态。

所以,问题是:我们不应该也宣布一个圣战到常量,并一直使用所有现代的东西,如DI,IoC或其他时尚的词?

一般来说,是的,避免常量。他们将消费者的耦合引入全球范围。也就是说,消费者依赖外部的东西。这是不明显的,例如

class Foo
{
    public function doSomething()
    {
        if (ENV === ENV_DEV) {
            // do something this way
        } else {
            // do something that way
        }
    }
}

如果不了解doSomething的内部结构,您将不会知道存在对具有该常数的全局范围的依赖。因此,除了使代码更难理解之外,还限制了它的重用方式。

上述情况也适用于只有一个值的常量,例如

public function log($message)
{
    fwrite(LOGFILE, $message);
}

这里的常量将指向在外部某处定义的文件资源

define('LOGFILE', fopen('/path/to/logfile'));

这与使用ENV一样不明显。它是一种依赖关系,需要类外的东西才能存在。我必须知道这一点才能处理那个对象。由于使用此常量的类隐藏了此详细信息,因此我可能会尝试在不确保常量存在的情况下记录某些内容,然后我想知道为什么它不起作用。它甚至不必是资源,LOGFILE可以简单地将路径作为字符串包含。相同的结果。

依赖使用者中的全局常量还需要在单元测试中设置全局状态。这是您通常希望避免的事情,即使常量是固定值,因为单元测试的重点是隔离测试单元,并且必须将环境置于特定状态会阻碍这一点。

此外,使用全局常量总是会带来不同库常量冲突的威胁。根据经验,不要将任何东西放入全局范围。如果必须使用命名空间,请使用它们来聚类常量。

但是,请注意,命名空间常量在耦合方面仍然存在相同的问题,类常量也是如此。只要这种耦合位于同一命名空间中,它就不那么重要,但是一旦您开始耦合到来自各种命名空间的常量,您就会再次阻碍重用。就此而言,请考虑任何常量公共 API。

使用

常量的替代方法是使用不可变的值对象,例如:

class Environment
{
    private $value;
    public function __construct($value)
    {
        $this->assertValueIsAllowedValue($value);
        $this->value = $value;
    }
    public function getValue() {
// …

这样,除了确保这些值有效之外,您还可以将这些值传递给需要它们的对象。像往常一样,YMMV。这只是一种选择。单个常量不会使您的代码无法使用,但主要依赖常量会产生不利影响,因此根据经验,请尝试将它们降至最低。

在相关的旁注中,您可能还对以下内容感兴趣:

  • 接口常量的优缺点和
  • PHP 全局函数

全局变量被认为是不好的做法的主要原因是它们可以在系统的一部分中修改并在另一部分中使用,而这两段代码之间没有直接链接。

这会导致潜在的错误,因为编写使用全局变量的代码时,可能不知道(或考虑)使用全局变量的所有位置以及可以更改它的方式。反之亦然,编写对全局进行更改的代码,而没有意识到更改可能对代码的其他不相关部分产生的影响。

常量不共享此问题,因为它们是...嗯,不变。一旦定义,就无法更改它们,因此不会发生上一段中描述的发布。

因此,它们可以在全球范围内使用。

也就是说,我见过一些写得不好的PHP代码,它们使用define来创建常量,但在不同情况下以不同的方式声明常量。这是对常量的误用:常量应该是绝对固定的值;它应该只是一个值。如果你有一些东西在程序的不同运行中可能是不同的值,那么它不应该被定义为一个常量。这种事情确实应该是一个变量,然后应该遵循与其他变量相同的规则。

这种误用只能在像PHP这样的脚本语言中发生;在编译语言中不可能发生,因为你只能在一个地方定义一个常量一次,并且是一个固定的值。

全局变量和全局常量之间有很大的区别。

全局变量被回避的主要原因是因为它可以随时被任何东西修改。它可以在调用/执行顺序上引入各种隐藏的依赖关系,并且可能导致相同的代码有时工作而在其他代码中不起作用,具体取决于全局是否以及如何更改。显然,如果您正在处理并发性或并行性,那么糟糕的魔力可能会进一步增加。

全局常量在整个代码中始终(或应该)完全相同。一旦您的代码开始执行,就可以保证查看它的每一位代码每次都会看到相同的内容。这意味着没有引入意外依赖的危险。事实上,使用常量对于提高可靠性非常有用,因为这意味着如果需要更改代码,则无需更新多个位置的值。(永远不要低估人为错误!

单例是另一个问题。这是一种经常被滥用的设计模式,基本上可以最终成为全局变量的面向对象版本。在某些语言(如 C++)中,如果您不注意初始化顺序,它也可能会出错。但是,它有时可能是一种有用的模式,尽管通常有更好的替代方案(尽管有时需要稍微多做一些工作)。

编辑:简单地扩展一下,您在问题中提到全局常数引入了"有史以来最全局的状态"。这并不准确,因为全局常量是(或应该)固定的,其方式与源代码的固定方式相同。它定义了程序的静态性质,而"状态"通常被理解为动态运行时概念(即可以更改的东西)。