PHP中的值对象与关联数组

45

(这个问题以PHP为背景,但并不仅限于PHP。例如任何具有内置哈希的语言也是相关的)

让我们看一下这个例子(PHP):

function makeAFredUsingAssoc()
{
    return array(
        'id'=>1337,
        'height'=>137,
        'name'=>"Green Fred");
}

对比:

class Fred
{
    public $id;
    public $height;
    public $name;

    public function __construct($id, $height, $name)
    {
        $this->id = $id;
        $this->height = $height;
        $this->name = $name;
    }
}

function makeAFredUsingValueObject()
{
    return new Fred(1337, 137, "Green Fred");
}

当然,方法#1更加简洁,但是它很容易导致错误,例如

$myFred = makeAFredUsingAssoc();
return $myFred['naem']; // notice teh typo here
当然,人们可能会争论$myFred->naem同样会导致错误,这是正确的。但是,对我来说,有一个正式的类感觉更加严格,但我无法真正证明它。
使用每种方法的优缺点是什么,人们应该在何时使用哪种方法?
11个回答

34

在表面下,这两种方法是等效的。然而,当使用类时,您可以获得大部分标准OO(面向对象)的好处:封装、继承等。

另外,请看以下示例:

$arr['naem'] = 'John';

这是完全有效的,可能会是一个难以找到的bug。

另一方面,

$class->setNaem('John');

永远不会起作用。


7
一旦你去掉了公有成员变量,这个类的优点就显而易见了。 - Frank Farmer
2
我很想请有经验的人扩展一下这个答案,讲解一下如何将其与编写可测试代码(PHPUnit)联系起来,以及使用其中一个的优缺点是什么? - farinspace

30

像这样的简单类:

class PersonalData {
    protected $firstname;
    protected $lastname;

    // Getters/setters here
}

相比数组有几个优点。

  1. There is no possibility to make some typos. $data['firtsname'] = 'Chris'; will work while $data->setFirtsname('Chris'); will throw en error.
  2. Type hinting: PHP arrays can contain everything (including nothing) while well defined class contains only specified data.

    public function doSth(array $personalData) {
        $this->doSthElse($personalData['firstname']); // What if "firstname" index doesn't exist?
    }
    
    
    public function doSth(PersonalData $personalData) {
        // I am guaranteed that following method exists. 
        // In worst case it will return NULL or some default value
        $this->doSthElse($personalData->getFirstname());
    }
    
  3. We can add some extra code before set/get operations, like validation or logging:

    public function setFirstname($firstname) {
        if (/* doesn't match "firstname" regular expression */) {
            throw new InvalidArgumentException('blah blah blah');
        }
    
    
    <pre><code>if (/* in debbug mode */) {
        log('Firstname set to: ' . $firstname);
    }
    
    
    $this-&gt;firstname = $firstname;
    

    }
  4. We can use all the benefits of OOP like inheritance, polymorphism, type hinting, encapsulation and so on...
  5. As mentioned before all of our "structs" can inherit from some base class that provides implementation for Countable, Serializable or Iterator interfaces, so our structs could use foreach loops etc.
  6. IDE support.
唯一的缺点似乎是速度。创建一个数组并对其进行操作会更快。然而,我们都知道在许多情况下,CPU 时间比程序员时间便宜得多。;)

1
我完全同意特别是第二点。它激发了我下面的答案。 - kizzx2
1
我认为“验证和记录”可以用数组更好地完成,因为您可能需要一起验证而不是独立地验证。 - prograhammer

8

经过一段时间的思考,这是我的答案。

相比数组,更喜欢值对象的主要原因是清晰度

考虑这个函数:

// Yes, you can specify parameter types in PHP
function MagicFunction(Fred $fred)
{
    // ...
}

对比

function MagicFunction(array $fred)
{
}

意图更加明确。功能作者可以强制执行他的要求。

更重要的是,作为用户,我可以轻松查找“有效Fred”的构成方式。我只需要打开Fred.php并发现它的内部结构。

调用方和被调用方之间存在一个合同。使用值对象,这个合同可以被写成经过语法检查的代码:

class Fred
{
    public $name;
    // ...
}

如果我使用数组,我只能希望我的用户会阅读注释或文档:
// IMPORTANT! You need to specify 'name' and 'age'
function MagicFunction(array $fred)
{
}

6
根据使用情况,我可能会使用其中的任何一种。类的优点是可以像类型一样使用,并在方法或任何内省方法上使用类型提示。如果我只想传递查询结果或其他随机数据集,我可能会使用数组。所以我想只要“Fred”在我的模型中具有特殊含义,我就会使用类。
另外一提:
值对象应该是不可变的。至少如果你是参考域驱动设计中Eric Evan的定义。在Fowler的PoEA中,值对象不一定是不可变的(尽管建议如此),但它们不应该有标识,而这显然是“Fred”的情况。

3

让我向您提出这个问题:

$myFred['naem'] 或者 $myFred->naem 这样打错字有什么不同呢?两种情况都存在相同的问题,并且它们都会报错。

我喜欢在编程时使用 KISS(保持简单,愚蠢)。

  • 如果您只是从方法中返回查询的子集,请简单地返回一个数组。
  • 如果您将数据存储为您的类中的 public/private/static/protected 变量,则最好将其存储为 stdClass。
  • 如果您稍后要将其传递给另一个类方法,则可能更喜欢 Fred 类的严格类型,即 public function acceptsClass(Fred $fredObj)

如果它作为返回值使用,您也可以创建一个标准类,而不是数组。在这种情况下,您可能不太关心严格类型。

$class = new stdClass();
$class->param = 'value';
$class->param2 = 'value2';
return $class;

有人为什么会更喜欢使用stdClass而不是关联数组吗?它们的功能在我看来几乎相同。关联数组语法更清晰简单。 - kizzx2
1
@kizz - 主要是在需要存储对象数组以便更轻松地进行迭代时使用,就像数据库抽象层返回对象数组一样(每个对象表示一行)。 - Corey Ballou
2
stdClass可以像数组一样进行迭代。使用stdClass与关联数组相比没有立即的优势,并且您将无法访问所有很酷的数组函数。是的,当访问公共属性时仍然可能发生拼写错误,但其他示例使用了公共方法,如果存在拼写错误,则无法正确编译。如果要使用值对象,您应该至少考虑不可变性。 - Charles Sprayberry

2
优点:哈希表可以处理在设计时未知的名称-值组合。

一个类也可以。如果你只是实例化一个类,然后做一些类似 $class->madeup = 'hello'; echo $class->madeup; 这样的事情,它将会输出 'hello' :) (当然,这只适用于使用 PHP 类而不是其他编程语言时)。 - AntonioCS
嗯...你说得对。这将在所有允许在运行时修改类的动态语言中得到支持。这就是使用哈希的优点所在... - Veger

1
一个正确的值对象的好处在于,没有办法制造出无效的值对象,也没有办法更改已经存在的值对象(完整性和“不可变性”)。只有getter和类型提示参数,没有办法在可编译的代码中搞砸它,而你显然可以轻松地用可塑性数组来做到这一点。
或者你可以在公共构造函数中进行验证并抛出异常,但这提供了一个更温和的工厂方法。
class Color
{
    public static function create($name, $rgb) {
        // validate both
        if ($bothValid) {
            return new self($name, $rgb);
        } else {
            return false;
        }
    }
    public function getName() { return $this->_name; }
    public function getRgb() { return $this->_rgb; }

    protected function __construct($name, $rgb)
    {
        $this->_name = $name;
        $this->_rgb = $rgb;
    }
    protected $_name;
    protected $_rgb;
}

1

当返回值代表应用程序中的实体时,应使用对象,因为这是面向对象编程的目的。如果您只想返回一组不相关的值,则情况就不那么明确了。但如果它是公共API的一部分,则声明类仍然是最好的选择。


我非常努力地尝试想出一个实际的场景,在这个场景中我想返回一组不相关的值,但是失败了。也许在读取CSV文件时可能会用到,但PHP的fgetcsv()函数只返回数值数组,所以... - kizzx2

1

说实话,我两个都喜欢。

  • 哈希数组比制作对象快得多,时间就是金钱!
  • 但是,JSON不喜欢哈希数组(这似乎有点像OOP强迫症)。
  • 也许对于多人参与的项目,定义良好的类会更好。
  • 哈希数组可能需要更多的CPU时间和内存(对象具有预定义的数量),尽管很难确定每种情况。

但真正糟糕的是过度思考要使用哪一个。就像我说的,JSON不喜欢哈希。哎呀,我用了一个数组。现在我必须更改几千行代码。

我不喜欢它,但似乎类是更安全的选择。


4
json_encode(array)有什么问题? - Peter Krauss

1

我已经使用面向对象编程语言工作了10年以上。 如果你理解对象的工作方式,你会喜欢它。 继承、多态、封装、重载是面向对象编程的关键优势。 另一方面,当我们谈论PHP时,我们必须考虑到PHP不是一个完整的面向对象语言。 例如,我们不能使用方法重载或构造函数重载(直接)。

在PHP中,关联数组是一个非常好的特性,但我认为这会损害PHP企业应用程序。 当你编写代码时,你希望得到一个干净和可维护的应用程序。

使用关联数组失去的另一个东西是你无法使用智能感知。

因此,我认为如果你想编写更清洁和更易于维护的代码,你必须在提供面向对象编程功能时使用它们。


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