【问题标题】:Immutable object with many properties具有许多属性的不可变对象
【发布时间】:2018-05-11 13:29:53
【问题描述】:

我有一个类,我想使其不可变,但该类有很多属性。

<?php

class Gameworld {

    /** @var string */
    private $name;

    /** @var string */
    private $type;

    /** @var bool */
    private $is_online;

    /** @var int */
    private $online_players;

    /** @var int */
    private $online_players_record;

    /** @var string */
    private $description;

    /** @var string */
    private $location;

    /** @var \DateTime */
    private $created_at;

}

如何创建这样的对象?当我介绍带有所有这些属性的public function __construct() 时,它会变得臃肿。如果我引入 setter,它将不再是不可变的。


编辑:我正在考虑制作二传手,但只能使用一次。多亏了这一点,我不会有臃肿的构造函数,但由于某种原因,这似乎不是一个好主意。就像是:

class Gameworld {

    ... old properties ...

    /** @var array */
    private $used_setters = [];

    public function setName(string $name){
        if(in_array('name', $this->used_setters)){
            throw new ImmutableException('Class Gameworld is immutable.');
        }

        $this->name = $name;
        $this->used_setters[] = 'name';
    }

}

【问题讨论】:

  • 私有属性不是不可变的。你需要常量。
  • 有一个RFC for that,但仍在讨论中。看看金钱比较。
  • @GabrielHeming 我知道那个 RFC,但我正在寻找当前状态的解决方案。它仍然不能解决我的问题,因为我需要创建填充了所有这些属性的对象,我正在考虑要么制作丑陋的public function __construct(string $name, string $type, bool $is_online, int $online_players, int $online_players_record, string $description, string $location, \DateTime $created_at),要么使用反射和setAccessible 填充这些属性的 Hydrator。
  • 只有两种方法可以在不创建新对象的情况下填充不可变对象。使用它的__construct(它确实创建了一个对象,但不是一个新对象)或通过反射(有很多库可以做到这一点)。如果您使用set 方法(没有问题),它将始终创建一个新对象,而原始对象仍然是不可变的。但是,如果您只想创建一个不改变其属性的对象,甚至随着时间的推移创建一个新对象,请使用常量并阻止__clone。顺便说一句,为什么这是不可变的?也许另一种方法可以解决您的问题。
  • 所有属性都应该是不可变的吗?还是只有一些?如果是这样,哪些?可能的值是什么?

标签: php immutability


【解决方案1】:

我会使用__set()魔术方法来检查是否设置了值,否则我会抛出异常:

class Gameworld {

    /** @var string */
    private $name;

    /** @var string */
    private $type;

    /** @var bool */
    private $is_online;

    /** @var int */
    private $online_players;

    /** @var int */
    private $online_players_record;

    /** @var string */
    private $description;

    /** @var string */
    private $location;

    /** @var \DateTime */
    private $created_at;

    public function __set($property, $value)  
    {  
        if (property_exists($this, $property)) {  
            if(is_null($this->$property)){
                $this->$property = $value;
                return $this;
            }
            throw MyCustomException();
        }
        throw UndefinedClassVariableException();
    }
}

基本用法是:

$x = new Gameworld();
$x->name = "OK";
$x->is_online = true;
$x->name="Exception";

P.S:异常必须extend the base exception class 或者你可以triggerlog 一个错误,取决于你想要什么

P.S.S:另一种解决方案是使用__call() 方法并检查值是否为空

【讨论】:

  • 所以它类似于我在编辑时的propsition。我看到这个解决方案有两个缺点:首先是它不能确保数据的完整性,其次是一个值实际上可以是 null 从开始。
  • @simivar 所以可能保留实际设置属性的私有数组,而不是“is_null”使用“in_array($this->userProps)”?
  • 数据完整性应该在检索之前或持久性(单点事实)之前完成。它们确实可以为空,您可以一次设置它们,也可以一个接一个地设置它们(我建议使用工厂设计模式来确保设置所有属性)
【解决方案2】:

我建议将属性作为数组参数传递,例如

__construct( array $properties )
{
  $this->property = $properties['property'];
  ....
}

【讨论】:

  • 这似乎是目前最好的方法,但遗憾的是我无法确保每个属性都有有效的类型(字符串、整数等)。
  • 可能有点矫枉过正,但你可以有一个 GamewordProperties 类来确保属性的完整性,然后是 __construct(GamewordProperties $properties) { ... }
  • @CD001 我对这个解决方案感到很伤心。看起来很棒,但我仍然在做与主要课程几乎相同的二等课程。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-20
  • 1970-01-01
  • 2012-07-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多