PHP 中的不可变对象?

18

在 PHP 中创建不能改变的对象是一个好主意吗?

例如,一个日期对象具有设置器方法,但它们将始终返回一个新的对象实例(带有修改后的日期)。

对于使用该类的其他人来说,这些对象是否会令人困惑,因为在 PHP 中通常期望对象可变?

示例

$obj = new Object(2);

$x = $obj->add(5); // 7
$y = $obj->add(2); // 4

你认为拥有这样的类有什么好处吗?我认为更好的方法可能是简单地拒绝在不想更改的属性上进行设置,如果需要更改,则强制创建一个新实例。编辑:是的,就像ThiefMaster所说的那样 :) - Rudi Visser
这个例子可以帮助你理解。 - misha-from-lviv
希望不久后能有不变性功能的推出 https://wiki.php.net/rfc/immutability - Hoang Viet Nguyen
7个回答

29

不可变对象没有setter方法。就是这样。

每个人都会期望setXyz()方法具有void返回类型(在松散类型的语言中则不返回任何内容)。如果您向不可变对象添加setter方法,它将使人们感到困惑并导致丑陋的错误。


1
它们不会被称为set(),但我不知道该如何称呼它们。addetter方法? :) - Elfy
2
尽管“所有人”中的一些人不会感到惊讶地收到setter对象的实例,以允许方法链接;-) - Johan Fredrik Varen
12
称它们为“with”方法。 $obj->withMyProp($x) 返回一个新实例,其中“MyProp”已设置为 $x - mpen
确实。在Java中,这是相当常见的[1] [2]。 - MC Emperor
1
这是所有正常编程语言的正确答案。但这毕竟是关于PHP的。PHP有一些非常出色的核心方法,例如 DateTimeImmutable :: setTime()......它让我的头晕。 - jlh

16

我认为值对象应该是不可变的。除此之外,除非您要在整个应用程序中共享对象,否则它没有太多好处。

这里有一些错误的答案,一个不可变的对象可以有setter。以下是PHP中不可变对象的一些实现示例。

示例#1。

class ImmutableValueObject
{
    private $val1;
    private $val2;

    public function __construct($val1, $val2)
    {
        $this->val1 = $val1;
        $this->val2 = $val2;
    }

    public function getVal1()
    {
        return $this->val1;
    }

    public function getVal2()
    {
        return $this->val2;
    }
}

正如您所看到的,一旦实例化,您就无法更改任何值。

示例2:使用setters

class ImmutableValueObject
{
    private $val1;
    private $val2;

    public function __construct($val1, $val2)
    {
        $this->val1 = $val1;
        $this->val2 = $val2;
    }

    public function getVal1()
    {
        return $this->val1;
    }

    public function withVal1($val1)
    {
        $copy = clone $this;
        $copy->val1 = $val1;

        return $copy;    // here's the trick: you return a new instance!
    }

    public function getVal2()
    {
        return $this->val2;
    }

    public function withVal2($val2)
    {
        $copy = clone $this;
        $copy->val2 = $val2;

        return $copy;
    }
}

有几种实现方式可供选择,这绝不是一个排他性的列表。请记住,在PHP中,通过反射总是有办法解决问题,因此最终数据的不变性只存在于你的思想中!

将不可变对象标记为final通常也是一个好习惯。

编辑:

  • 将setX更改为withX
  • 添加有关final的评论

1
return $this; 应该返回 $copy 吗? - Kanstantsin K.
3
在计算机科学中,mutator方法是用于控制变量更改的方法。它们也被广泛称为setter方法。不可变对象不能有setter。它们的方法不会改变对象的内部状态,而只是创建一个新实例。这是一个很大的区别。 - John Smith
6
使用“setX”或“changeX”这样的名称是不好的。这些名称强烈暗示对象的内部状态将发生变化,但实际上并非如此。更好的做法是例如使用“withX”。 - John Smith
一个不可变对象不能有setter。它的方法不会改变对象的内部状态,只是创建一个新实例。同意,这就是不可变对象中setter的作用。虽然我同意,在这种情况下,将其命名为setX或changeX是错误的。 - Théo

5

不变对象在初始创建后无法更改,因此拥有setter方法与这个基本原则相违背,毫无意义。

你可以通过操作类成员可见性和覆盖魔术方法__set()来实现一些解决方案以模拟PHP中的不变性,但不能保证是真正的不变性,因为不变性不是PHP语言的特性。

我相信有人曾经编写了一个扩展程序,在PHP中提供不变的值类型。因此,你可以搜索一下。


3
关于"拥有setter方法没有意义"的说法:一个名为add的方法(就像问题中提到的一样)在不可变对象中可能是有意义的,通过返回一个状态已修改的新对象来实现。 - Matteo T.

1
在PHP中使对象变为不可变的非常容易。以下是一种优雅且方便的方法。
您需要做的就是创建具有特定__get()__set()魔术方法的基本抽象类,并在子对象中扩展此基类。
如果您使用值对象(例如DDD),则这非常适用。
以下是基本类:
abstract class BaseValueObject
{
    public function __get(string $propertyName)
    {
        return $this->$propertyName;
    }

    public function __set(string $propertyName, $value): void
    {
        throw new \Exception("Cannot set property {$propertyName}. The object is immutable.");
    }
}

现在有一个子对象(嗯,它的类)。
class CategoryVO extends BaseValueObject
{

    public $id;
    public $name;

    public function __construct(array $data)
    {
        $this->id = $data['id'];
        $this->name = $data['name'];
    }
}

如果尝试设置某个值,它将抛出异常。基本上它是不可变的。

就是这样。

制作尽可能多的不可变对象。通过构造函数创建新对象。需要时处理并重新创建新对象(如果需要,添加特定的创建方法,静态或实例方法,到基类或扩展类中)。

然而,这样的对象将方便地将其所有属性公开为只读属性(用于某种序列化或类似操作),而不像我们将它们设置为私有属性(但即使如此,我们也可以使用 JsonSerializable 接口,以使序列化与私有属性一样灵活或进行更彻底的转换)。

最后,由于它是一个抽象类,因此无法错误地实例化 BaseValueObject。从各个角度来看,这是一个漂亮而优雅的解决方案。


0

如果你想在一个类和对象上使用setter,这是完全可以的,因为我们经常需要设置对象数据。只是不要称它为immutable。

开发世界中有很多主观因素——我们的方法、方法论等等——但"immutable"是一个相当明确的定义:

"Immutable":
- 随时间不变或无法改变。

如果你想要一个immutable对象,那么它在实例化之后就不能被更改。这对于像来自数据库的数据这样需要保持不变的东西非常有用。

如果你需要在实例化之后调用对象并设置或更改数据,那么这就不是一个immutable对象。

你会把一辆车上的两个轮子拆下来然后称它为摩托车吗?

关于"immutable"类上的方法是否应该命名为不带"set"的名称,有些讨论,但这并不能阻止它们成为设置数据的方法。你可以称其为thisDoesNotSetAnything(int $id),并允许传入数据来更改对象。它将成为一个setter,因此对象是可变的。


0

我创建了一个小特性,避免使用反射来简化不可变性的实现: https://github.com/jclaveau/php-immutable-trait

显然,由于它不是语言特性,它不会通过魔法阻止突变,但可以减轻必须在应用之前克隆当前实例的变异器代码。如果应用于Massimiliano的示例,则会产生

class ImmutableValueObject
{
    use JClaveau\Traits\Immutable;

    private $val1;
    private $val2;

    public function __construct($val1, $val2)
    {
        $this->val1 = $val1;
        $this->val2 = $val2;
    }

    public function getVal1()
    {
        return $this->val1;
    }

    public function withVal1($val1)
    {
        // Just add these lines at the really beginning of methods supporting
        // immutability ("setters" mostly)
        if ($this->callOnCloneIfImmutable($result))
            return $result;

        // Write your method's body as if you weren't in an Immutable class
        $this->val1 = $val1;
        return $this;
    }

    public function getVal2()
    {
        return $this->val2;
    }

    public function withVal2($val2)
    {
        if ($this->callOnCloneIfImmutable($result))
            return $result;

        $this->val2 = $val2;

        return $this;
    }
}
  • 你可以看到,你在这里返回的不是$copy而是$this,正如Kanstantsin K所指出的。
  • 在本机PHP中,https://secure.php.net/manual/en/class.datetimeimmutable.php具有会返回应用修改的新实例的变异体。因此,复制并粘贴句子说不可变对象不应该有变异体似乎并不是非常有趣。
  • 使用“withXXX”而不是“setXXX”的做法非常有趣,感谢您的建议!我个人为API更改实例的可变性(Trait SwitchableMutability中的可选API)使用“becomesXXX”。

希望它能帮助一些人!

PS:对于这个小功能的建议真的非常欢迎 :) :https://github.com/jclaveau/php-immutable-trait/issues


0

从不可变对象中,您可以获取其值,但无法修改它们。下面是一个不可变类的示例:

<?php 

declare(strict_types=1);

final class Immutable 
{
    /** @var string */
    private $value;

    public static function withValue(string $value): self
    {
        return new self($value);
    }

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

    public function value(): string 
    { 
        return $this->value;
    }
}

// Example of usage:
$immutable = Immutable::withValue("my value");
$immutable->value(); 

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