构造函数应该抛出异常吗?PHP面向对象编程中的两种观点,哪种更好

构造函数应该抛出异常吗?

假设你要创建一个类的对象,但在创建过程中出现了某种错误,这可能是因为传入了错误的参数,或者在创建对象时第三方服务(例如数据库)不可用。

如果出现这种情况,显然需要正确处理错误,但问题是,你会在构造函数中抛出异常,还是在其他地方处理错误情况呢?

需要注意的是,本文将以PHP为例进行讨论,但同样的讨论也适用于其他实现了面向对象编程原则的语言。在Drupal开发Drupal模块开发以及Drupal升级等相关工作中,也会涉及到类对象的创建和错误处理,理解构造函数是否抛出异常的问题十分关键,尤其是在Drupal11的开发环境中。

我公司对这个主题进行了一些研究,发现关于这个问题大致有两种观点,分别是:

  • 如果出现问题,对象应该存在,但不可用。
  • 如果出现问题,如果对象不可用,则不应该存在

下面让我们详细探讨这两种观点。

一、对象应该存在

如果创建对象时出现问题,这并不重要,对象应该以某种形式存在。这种方法适用于极简构造函数,这类构造函数只是将输入参数赋值给属性,几乎不做其他操作。

例如,以下类有一个名为“number”的属性,可以在构造函数中创建。

class ObjectWillExist {

  public int $number;

  public function __construct($number) {
    if ($number !== 0) {
      $this->number = $number;
    }
  }
}

如果将数字设置为“0”,则该属性不会被设置。

$object = new ObjectWillExist(0);
var_dump($object);

这将产生以下输出。

object(ObjectWillExist)#1 (0) {
  ["number"]=>
  uninitialized(int)
}

在上述示例中,检测数字“0”是一个任意条件,但如果该属性包含数据库连接或curl句柄,整体效果将类似。对象将存在,但实际上处于损坏状态。

为了使这种方法有效,该类还应该有辅助方法,用于检查对象的当前状态。如果没有这些方法,就有可能调用不存在的资源并产生错误。

另一种方法是完全去掉构造函数,倾向于使用“init”或“connect”函数来准备好使用资源。然而,这些方法有点像代码异味,你要么强制开发人员在使用实际方法之前调用一个方法,要么需要将相同的检查复制粘贴到所有类方法中。

我公司应该提到,从PHP 8.0开始,可以使用构造函数属性提升通过构造函数自动注入对象属性。以这种方式注入$number属性的语法如下。

class ObjectWillExist {
  public function __construct(protected int $number) {}
}

无论我们如何设置,对象都会被创建并以某种方式可用。不过,理想情况下,它应该有一些代码来防止在上游代码中引发错误。

二、对象不应该存在

在这种情况下,构造函数往往不仅仅是分配变量,它会连接到数据库或创建curl句柄,使对象处于可用状态。

关键是,如果任何这些步骤失败,构造函数将抛出错误,并且不会创建对象。

让我们修改上面的示例,以便如果将数字“0”传递给构造函数,则抛出异常。

class ObjectMayExist {

  public int $number;

  public function __construct($number) {
    if ($number === 0) {
      throw new Exception('Invalid value');
    }
    $this->number = $number;
  }
}

为了使这种方法有效,上游代码需要能够处理构造函数可能抛出的错误。

try {
  $object = new ObjectMayExist(0);
} catch (Exception $e) {
  // 对象未被创建。
}

在上面的示例中,由于构造函数在赋值之前抛出错误,$object变量永远不会被设置。

这种方法更清晰直接,你要么得到一个可以立即使用的对象,要么得到一个错误。因此,你不需要在类中添加任何辅助代码来检测对象是否已正确创建。如果对象存在,它将按预期工作。

这种方法唯一的问题是,在大型应用程序中,你需要更加小心,尤其是在使用依赖注入机制时。如果对象是注入依赖项的一部分,那么该依赖项可能也无法创建,并可能导致一系列错误。在Drupal开发Drupal模块开发以及Drupal升级过程中,这种依赖关系的处理尤为重要,特别是在Drupal11的开发环境下。

三、结论

我公司一直支持“对象应该存在”的观点,但最近一直在思考这个问题,开始认为“对象不应该存在”的方法更有意义。代码需要防范异常,但这意味着在初始化或使用对象之前检查其当前状态的代码会更少。

我公司开始回顾这些年来编写的一些代码,以及如何使用初始化和检查方法来检查对象的内部状态。做这类事情总是感觉像代码有异味,但当时没有想到在构造函数中抛出错误的可能性。现在知道了这种技术,将来会认真使用它。

你认为这里最好的方法是什么?是否有我公司没有考虑到的第三种观点?请在评论中告诉我!