改变对象以避免副作用的正确方法是什么?

What is the right way to mutate objects to avoid side effects?

我有下一个对象系统(简单示例):

class Grid
{
    public State $state;
    // Other fields

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

    // ...
}

class State
{
    public bool $isCompleted = false;
    public ?User $judge;
}

class User
{

}

免责声明:Grid class 是遗留的 ActiveRecord 模型,它不能被独立的单元测试覆盖,因为它写入数据库并更改系统中的一些其他数据。所以我只对 State class.

感兴趣

我需要 State 的突变体 class。它必须易于测试。它看起来像这样:

class StateMutator
{
    public function mutate(State $state, array $changes):?State
    {
        // ...
        $state->isCompleted = true;
        // ...
        if(!$someCondition){
            return null;
        }
        // ...
        return $state;
    }
}

并且是这样使用的:

/** @var Grid $grid */
/** @var array $changes */
$newState = (new StateMutator())->mutate($grid->state, $changes);
if($newState !== null){
    $grid->state = $newState;
}
// Some other changes in $grid
$grid->saveChanges();

看起来不错。但是有些事情让我感到困惑。如果 mutator 在获得的对象中做了一些更改,然后 returns null,那么调用代码会认为 State 没有改变 - 在其中进行一些其他更改并将其保存到数据库中。但是因为 PHP 通过引用传递对象,所以 mutator 在状态对象中所做的更改也将保存到数据库中。这是一个问题。

我应该怎么做才能避免这个问题?

我有两种方法可以解决这个问题,但是它们都有很大的问题。

  1. 如果修改器不能在它内部的任何地方改变状态对象,它应该恢复它已经完成的改变。但在某些情况下很难做到甚至不可能。
  2. 变异器应该克隆状态,变异它的副本并return它。但在这种情况下,该方法将需要更多的内存(状态中可以有超过 1000 个对象 属性)。

有人知道吗?

我通常会避免对象进入无效状态。您的 State 对象应该具有改变其状态或 return 具有新状态的克隆的方法。这些方法仅在结果状态有效时才验证输入和 mutate/return 克隆。无需跟踪更改和回滚。