PHP 7 返回类型提示

5

关于PHP 7返回类型提示及为什么定义为返回类型的类不能返回null,已经有很多问题了。比如这个问题:Correct way to handle PHP 7 return types。 答案通常会说,如果函数应该返回一个类,则返回null可能是一个异常情况。 也许我错过了什么,但我真的不理解为什么。例如,我们来看一个简单的用户类:

class User
{
    private $username;  // mandatory
    private $password;  // mandatory
    private $realName;  // optional
    private $address;   // optional

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

    public function setUsername(string $username) {
        $this->username = $username;
    }

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

    public function setPassword(string $password) {
        $this->password = $password;
    }

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

    public function setRealName(string $realName = null) {
        $this->realName = $realName;
    }

    public function getAddress() : Address {
        return $this->address;
    }

    public function setAddress(Address $address = null) {
        $this->address = $address;
    }

}

在我们的应用程序中,拥有一个没有真实姓名和/或地址的用户是完全合法的。我甚至可以将真实姓名和地址字段设置为 null - 即使上面的解决方案不是最佳的。在我们的个人资料页面上,如果地址为空(null),我希望显示提示信息。
但这只是一个例子。我们的应用程序有100多个数据库表和相应的PHP类,几乎所有表都有可选字段。对于处理可选字段,总是编写 try catch 块而不是简单地检查 null,似乎对我来说有点次优。那么,对于上面的示例,有什么好的解决方案呢?

你可以将变量初始化为空的默认值(例如字符串为'',地址为一个带有空/无效/默认值的地址对象)。你也可以使用你的类,从一个包装器中调用它们,如果返回null,则该包装器会捕获异常。 - Nadir
我认为 getAddress().getCity === '' 并不比简单地检查 getAddress() == null 更好。此外,检查可能会变得复杂。例如,如果邮政编码是可选的,则新开发人员可以编写代码:getAddress().getZip() === '',并认为地址缺失,但实际上只有邮政编码缺失。 - Vmxes
在你的例子中,当$this->realName为空时,你期望调用代码调用getRealName()的行为是什么?它是否需要测试该值并在其为空时处理它?如果是这样:你的类应该这样做。如果您利用空字符串在许多字符串操作中被视为空字符串的特性,那么您可能应该将其实际默认设置为空字符串,而不是null。大多数情况都可以这样解决。尽管如此,我认为PHP在强制执行此操作时持有不适当的态度。 - Adam Cameron
1
可以使用Address::empty()来表示空地址,而不是使用null。尽管我个人不会对可选字段进行类型提示。 - apokryfos
@AdamCameron 调用者类(即UserProfile)应该处理这种情况。例如,在注册表单上只有用户名和密码字段存在。因此,realName应该保持为空,因为稍后在个人资料页面上,我可以显示填写真实姓名的提示。但是,如果用户不想要它,我们将简单地保存一个空字符串,以便下次不再打扰用户。因此,null和空字符串会产生不同的逻辑结果。 - Vmxes
2个回答

2
问题在于你的对象没有遵循面向对象编程(OOP)规则。
首先,getter和setter方法是有害的,因为它们暴露了对象的内部结构。这样就引入了无限的可能性,依赖于内部用户对象结构来为系统的其余部分提供接口。而对象的目的是隐藏实现细节并提供操作此隐藏数据的接口。
另外,null是有害的(或者说是一个价值十亿美元的错误),因为它使代码不够可靠。它引入了特殊情况(null返回值),需要在使用你的对象的代码中处理这个特殊情况。更糟糕的是,如果你忘记处理这个“null”特殊情况(或者你不知道有这个特殊情况),你将不会立即得到错误。所以你会得到隐藏的错误,这可能非常耗时去找到和修复。
实际上,我看到以下选项(在所有情况下-移除getter):
选项1) 以某种统一的形式获取配置文件数据的快照
$user->getProfileData() {
    return [
        'username': $this->username,
        'realName': $this->realName ? $this->realName : '',
        'address': $this->address ? $this->address : 'Not specified'
        ...
    ];
}

这仍然是一种getter,但是您将所有数据转换为统一格式(字符串)。即使在您的数据库中有一些整数或浮点字段(如年龄或身高),在此处也应返回字符串,以便系统的其余部分不依赖于对象的实际内部。可选字段的空字符串(或特殊值,例如“未指定”)也充当一种“null对象”。还可以将返回的值包装到小的字段对象中,并实际使用可选字段的null对象模式。使用类的代码中不应处理特殊的“null”情况,只需循环遍历字段并显示字符串(包括空字符串)。选项2)使对象本身负责表示。
$user->showProfile($profileView) {
    $profileView->addLabel('First Name');
    $profileView->addString($this->username);
    ...
}

在这里,您将对象的内部细节保留在对象内部,但现在它做得太多了,因此有人可能会说它现在违反了SRP

选项3)创建专门负责呈现的对象。

$userPresentation = $user->createPresentation()
// internally it will return new UserPresentation($this->username, this->realName, $this->address, ...);
// now display it - generate the template and insert it into the view
<? echo $userPresentation->getHtml(); ?>

在这里,您将演示逻辑移入单独的对象中。用户对象及其表示紧密耦合,但系统的其余部分现在不知道用户对象的内部情况(也没有机会了解)。


非常详细的回答,但是我认为在简单实体类的情况下,这三个选项都违反了SRP原则。我不想为用户属性的子集(取决于调用者类需要什么)总是创建一个新的getter和一个新的类。当然,我也不想在我的实体类中包含任何演示逻辑。 - Vmxes
@Vmxes 目前你的实现存在一个基本的面向对象编程原则——封装性的违反。虽然你将字段定义为“private”,但是你又通过getter和setter将这些字段暴露给了外部。而且你返回null,所以你必须在代码中到处检查特殊情况。一旦你开始思考如何摆脱这些缺陷并使用其他方法,你会发现你的其余代码也变得更简单和统一。我的示例只是我想到的一些选项,请还要查看我提供的文章。 - Borys Serebrov

1
我最近接受了这个想法,即null表示存在问题,因此当我看到它时,我知道出了什么问题。
在这些情况下,我倾向于使用Null对象设计模式。因此,要处理地址,您需要创建类似以下内容的东西:
class NullAddress implements AddressInterface { }

Address类还需要实现AddressInterface
然后,getAddress()看起来就像这样:

public function getAddress(): AddressInterface {
    return $this->address ?? new NullAddress();
}

调用 getAddress() 函数后,可以通过以下方式检查 null 值:

if ($user->getAddress() instanceof NullAddress) {
    // Implement result of empty address here.
}

在处理字符串时,迄今为止返回空字符串一直对我很有用。我只需使用empty()来检查它。


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