什么定义了有效的对象状态?

2022-08-30 20:04:35

我正在阅读一篇关于构造函数做太多工作的文章。一段内容如下:

在面向对象的样式中,依赖项倾向于反转,构造函数具有不同且更斯巴达的角色。它唯一的工作是确保对象初始化为满足其基本不变量的状态(换句话说,它确保对象实例以有效状态开始,仅此而已)。

下面是一个类的基本示例。在创建类时,我传入HTML,需要解析它以设置类属性。

OrderHtmlParser
{
    protected $html;

    protected $orderNumber;

    public function __construct($html)
    {
        $this->html = $html;
    }

    public function parse()
    {
        $complexLogicResult = $this->doComplexLogic($this->html);

        $this->orderNumber = $complexLogicResult;
    }

    public function getOrderNumber()
    {
        return $this->orderNumber;
    }

    protected function doComplexLogic($html)
    {
        // ...

        return $complexLogicResult;
    }
}

我称之为使用

$orderparser = new OrderHtmlParser($html);
$orderparser->parse()
$orderparser->getOrderNumber();

我使用函数是因为我不希望构造函数执行任何逻辑,因为上面的文章和本文都指出这是可怕的做法。parse

public function __construct($html)
{
    $this->html = $html;
    $this->parse(); // bad
}

但是,如果我不使用该方法,则我的所有属性(在此示例中只有一个)都将返回 。parsenull

这被称为处于“无效状态”的对象吗?

此外,它有点感觉我的解析方法是一个伪装的函数,这也被另一篇文章认为是坏的(尽管我不确定这是否仅在构造函数调用该方法时,当手动调用时或两者兼而有之)。无论如何,初始化方法在设置属性之前仍然在做一些复杂的逻辑 - 这需要在可靠地调用getters之前发生。initialise

因此,要么是我误解了这些文章,要么这些文章促使我认为也许我对这个简单类的整体实现是不正确的。


答案 1

通常,在构造函数中执行工作是一种代码气味,但实践背后的原因更多地与编程语言有关,而不是对最佳实践的看法。有一些真正的边缘情况会引入错误。

在某些语言中,派生类的构造函数自下而上地执行,而在其他语言中则自上而下地执行其构造函数。在PHP中,它们是从上到下调用的,您甚至可以通过不调用来停止链。parent::__construct()

这在基类中创建了未知的状态期望,并且更糟糕的是,PHP允许您在构造函数中首先或最后一个调用父级。

例如;

class A extends B {
     public __construct() {
           $this->foo = "I am changing the state here";
           parent::__construct(); // call parent last
     }
}

class A extends B {
     public __construct() {
           parent::__construct(); // call parent first
           $this->foo = "I am changing the state here";
     }
}

在上面的示例类中,它的构造函数以不同的顺序调用,如果在构造函数中做了很多工作,那么它可能不处于程序员期望的状态。BB

那么,如何解决您的问题呢?

这里需要两个类。一个将包含解析器逻辑,另一个包含解析器结果。

class OrderHtmlResult {
      private $number;
      public __construct($number) {
            $this->number = $number;
      }
      public getOrderNumber() {
            return $this->number;
      }
}

class OrderHtmlParser {
      public parse($html) {
          $complexLogicResult = $this->doComplexLogic($this->html);
          return new OrderHtmlResult($complexLogicResult);
      }
}

$orderparser = new OrderHtmlParser($html);
$order = $orderparser->parse($html)
echo $order->getOrderNumber();

在上面的示例中,如果该方法无法提取订单号,则可以返回该方法,或者抛出一个示例。但是这两个类都不会进入无效状态。parse()null

这种模式有一个名称,其中一个方法产生另一个对象作为结果以封装状态信息,但我记得它的名称。


答案 2

这被称为处于“无效状态”的对象吗?

是的。您完全正确,该方法是伪装的函数。parseinitialise

为了避免初始化解析,请保持懒惰。最懒惰的方法是消除字段并从函数内部解析它。如果您期望重复调用该函数和/或预计解析成本高昂,请保留该字段,但将它视为缓存。检查它的内部,并仅在第一次调用时解析它。$orderNumber$htmlgetOrderNumber()$orderNumbernullgetOrderNumber()


关于链接的文章,我原则上同意构造函数应该仅限于字段初始化;但是,如果这些字段是从文本块中解析的,并且期望客户端将使用大部分或全部解析的值,则延迟初始化几乎没有价值。此外,当文本解析不涉及IO或域对象时,它不应该妨碍黑盒测试,对于黑盒测试,急切初始化与延迟初始化是不可见的。new


推荐