在 PHP 中创建不能改变的对象是一个好主意吗?
例如,一个日期对象具有设置器方法,但它们将始终返回一个新的对象实例(带有修改后的日期)。
对于使用该类的其他人来说,这些对象是否会令人困惑,因为在 PHP 中通常期望对象可变?
示例
$obj = new Object(2);
$x = $obj->add(5); // 7
$y = $obj->add(2); // 4
在 PHP 中创建不能改变的对象是一个好主意吗?
例如,一个日期对象具有设置器方法,但它们将始终返回一个新的对象实例(带有修改后的日期)。
对于使用该类的其他人来说,这些对象是否会令人困惑,因为在 PHP 中通常期望对象可变?
示例
$obj = new Object(2);
$x = $obj->add(5); // 7
$y = $obj->add(2); // 4
不可变对象没有setter方法。就是这样。
每个人都会期望setXyz()
方法具有void返回类型(在松散类型的语言中则不返回任何内容)。如果您向不可变对象添加setter方法,它将使人们感到困惑并导致丑陋的错误。
$obj->withMyProp($x)
返回一个新实例,其中“MyProp”已设置为 $x
。 - mpenDateTimeImmutable :: setTime()
......它让我的头晕。 - jlh我认为值对象应该是不可变的。除此之外,除非您要在整个应用程序中共享对象,否则它没有太多好处。
这里有一些错误的答案,一个不可变的对象可以有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通常也是一个好习惯。
编辑:
return $this;
应该返回 $copy
吗? - Kanstantsin K.不变对象在初始创建后无法更改,因此拥有setter方法与这个基本原则相违背,毫无意义。
你可以通过操作类成员可见性和覆盖魔术方法__set()
来实现一些解决方案以模拟PHP中的不变性,但不能保证是真正的不变性,因为不变性不是PHP语言的特性。
我相信有人曾经编写了一个扩展程序,在PHP中提供不变的值类型。因此,你可以搜索一下。
add
的方法(就像问题中提到的一样)在不可变对象中可能是有意义的,通过返回一个状态已修改的新对象来实现。 - Matteo T.__get()
和__set()
魔术方法的基本抽象类,并在子对象中扩展此基类。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
。从各个角度来看,这是一个漂亮而优雅的解决方案。
如果你想在一个类和对象上使用setter,这是完全可以的,因为我们经常需要设置对象数据。只是不要称它为immutable。
开发世界中有很多主观因素——我们的方法、方法论等等——但"immutable"是一个相当明确的定义:
"Immutable":
- 随时间不变或无法改变。
如果你想要一个immutable对象,那么它在实例化之后就不能被更改。这对于像来自数据库的数据这样需要保持不变的东西非常有用。
如果你需要在实例化之后调用对象并设置或更改数据,那么这就不是一个immutable对象。
你会把一辆车上的两个轮子拆下来然后称它为摩托车吗?
关于"immutable"类上的方法是否应该命名为不带"set"的名称,有些讨论,但这并不能阻止它们成为设置数据的方法。你可以称其为thisDoesNotSetAnything(int $id)
,并允许传入数据来更改对象。它将成为一个setter,因此对象是可变的。
我创建了一个小特性,避免使用反射来简化不可变性的实现: 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;
}
}
希望它能帮助一些人!
PS:对于这个小功能的建议真的非常欢迎 :) :https://github.com/jclaveau/php-immutable-trait/issues
从不可变对象中,您可以获取其值,但无法修改它们。下面是一个不可变类的示例:
<?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();