“零”地狱以及如何摆脱它

空值使用时若不慎,会使您的生活难以忍受,您甚至可能不了解到底是什么导致它们如此痛苦。 让我解释一下。


预设值


我们都看到了一个采用许多参数的方法,但是其中有一半以上是可选的。 结果是这样的:

public function insertDiscount(   string $name,   int $amountInCents,   bool $isActive = true,   string $description = '',   int $productIdConstraint = null,   DateTimeImmutable $startDateConstraint = null,   DateTimeImmutable $endDateConstraint = null,   int $paymentMethodConstraint = null ): int 

在上面的示例中,我们想创建一个默认情况下适用于所有地方的折扣,但是在创建时它可能是无效的,仅适用于特定产品,仅在特定时间起作用,或者在用户选择特定付款方式时适用。

如果要为某种付款方式创建折扣,则需要按以下方式调用该方式:

 insertDiscount('Discount name', 100, true, '', null, null, null, 5); 

这段代码可以使用,但是对于阅读它的人来说是完全无法理解的。 对其进行分析变得极为困难,因此我们无法轻松支持该应用程序。

让我们逐个示例地讨论这个示例。

什么是有效折扣?


我们已经发现无限折扣适用于所有地方。 因此,有效折扣包含除我们以后可以添加的限制以外的所有内容。 参数isActive的默认值为true。 因此,该方法可以如下调用:

 insertDiscount('Discount name', 100); 

仅通过阅读代码,我不知道折扣会立即生效。 为了找出答案,我必须检查方法签名是否具有默认值。

现在,假设您需要阅读200行代码。 您确定要检查所调用方法的每个签名以获取隐藏信息吗? 我宁愿只是阅读代码而不必寻找任何东西。

负责描述的参数也是如此。 默认情况下,它是一个空字符串-这可能会在您希望看到描述的地方引起很多问题。 例如,它可以打印在支票上,但是由于它是空的,因此用户仅会看到带有金额的行旁边的空行。 系统不应允许这种情况发生。

我将这样重写此方法:

 public function insertDiscount(  string $name,  string $description,  int $amountInCents,  bool $isActive ): int 

由于我们决定稍后使用单独的方法添加这些限制,因此我完全删除了这些限制。 由于现在需要所有参数,因此可以按任何顺序排列它们。 我将描述放在名称之后,因为当它们靠近时,代码阅读起来会更好。

 insertDiscount(  'Discount name',  'Discount description',  100,  Discount::STATUS_ACTIVE ); 

我还将常量用于折扣活动状态。 现在,您无需查看方法的签名即可了解此参数的真实含义:很明显,我们正在创建主动折扣。 将来,我们可以进一步改进此方法(破坏者:使用值对象)。

添加约束


现在您可以添加各种限制。 为了避免零,零,零地狱,我们将创建单独的方法。

 public function addProductConstraint(  Discount $discount,  int $productId ): Discount; public function addDateConstraint(  Discount $discount,  DateTimeImmutable $startDate,  DateTimeImmutable $endDate ): Discount; public function addPaymentMethodConstraint(  Discount $discount,  int $paymentMethod ): Discount; 

因此,如果我们要创建一个具有一定限制的新折扣,我们将这样做:

 $discountId = insertDiscount(  'Discount name',  'Discount description',  100,  Discount::STATUS_ACTIVE ); addPaymentMethodConstraint(  $discountId,  PaymentMethod::CREDIT_CARD ); 

现在将其与原始通话进行比较。 您将看到它变得更加方便阅读。

对象属性为空


解决对象属性中的零也会引起问题。 我无法传达我经常看到这样的事情:

 $currencyCode = strtolower(  $record->currencyCode ); 

um! “不能将null传递给strtolower。” 发生这种情况是因为开发人员忘记了currencyCode可能为null。 由于许多开发人员仍然不使用IDE或抑制其中的警告,因此很多年以来人们可能不会注意到这一点。 该错误将继续出现在一些未读日志中,并且客户端将报告显然与此无关的周期性问题,因此没有人会去看这行代码。

我们当然可以在访问currencyCode的任何地方添加空检查。 但是随后我们将陷入另一种地狱:

 if ($record->currencyCode === null) {  throw new \RuntimeException('Currency code cannot be null'); } if ($record->amount === null) {  throw new \RuntimeException('Amount cannot be null'); } if ($record->amount > 0) {  throw new \RuntimeException('Amount must be a positive value'); } 

但是,正如您已经了解的那样,这不是最佳解决方案。 除了使您的方法混乱之外,您现在应该在所有地方重复该测试。 而且,每次添加另一个null属性时,请不要忘记再进行一次此类检查! 幸运的是,有一个简单的解决方案:值对象。

价值对象


值对象是功能强大但简单的事物。 我们试图解决的问题是有必要不断验证我们的所有属性。 但是我们这样做是因为我们不知道是否可以信任对象的属性,以及它们是否有效。 如果可以的话该怎么办?

要信任值,它们需要两个属性:它们必须经过验证,并且自验证以来不得更改。 看一看这个课程:

 final class Amount {  private $amountInCents;  private $currencyCode;  public function __construct(int $amountInCents, string $currencyCode): self  {    Assert::that($amountInCents)->greaterThan(0);    $this->amountInCents = $amountInCents;    $this->currencyCode = $currencyCode;  }  public function getAmountInCents(): int  {    return $this->amountInCents;  }  public function getCurrencyCode(): string  {    return $this->currencyCode;  } } 

我正在使用beberlei / assert包。 每当检查失败时,它将引发异常。 这与源代码中null的例外相同,除非我们将检查移至此构造函数。

由于我们使用类型声明,因此我们保证类型也是正确的。 因此,我们不能将int传递给strtolower。 如果使用的是不支持类型声明的旧版PHP,则可以使用此包通过->整数()和->字符串()检查类型。

创建对象后,无法更改值,因为我们只有getter,而没有setter。 这称为免疫。 添加final不允许扩展此类以添加setter或magic方法。 如果在方法参数中看到Amount $ amount,则可以100%确保其所有属性均已通过验证并且该对象可以安全使用。 如果这些值无效,则将无法创建对象。

现在,借助值对象,我们可以进一步改进示例:

 $discount = new Discount(  'Discount name',  'Discount description',  new Amount(100, 'CAD'),  Discount::STATUS_ACTIVE ) insertDiscount($discount); 

请注意,我们首先创建一个Discount,然后在内部使用Amount作为参数。 这样可以确保insertDiscount方法接收有效的折扣对象,并使整个代码块更易于理解。

零恐怖故事


让我们看一个有趣的情况,其中null对应用程序可能有害。 这个想法是从数据库中提取集合并对其进行过滤。

 $collection = $this->findBy(['key' => 'value']); $result = $this->filter($collection, $someFilterMethod); if ($result === null) {   $result = $collection; } 

如果结果为null,则使用原始集合作为结果? 这是有问题的,因为如果过滤方法找不到合适的值,则返回null。 因此,如果所有内容都被过滤掉,我们将忽略过滤器并返回所有值。 这完全破坏了逻辑。

为什么要使用原始集合? 我们永远不会知道。 我怀疑开发人员对null在这种情况下意味着什么有一定的假设,但事实证明这是错误的。

这是空值的问题。 在大多数情况下,不清楚它们的含义,因此我们只能猜测如何应对它们。 犯错很容易。 另一方面,例外非常清楚:

 try {  $result = $this->filter($collection, $someFilterMethod); } catch (CollectionCannotBeEmpty $e) {  // ... } 

此代码是唯一的。 开发人员不太可能会误解它。

值得付出努力吗?


所有这些似乎都需要额外的精力来编写执行相同操作的代码。 是的,是的。 但是与此同时,您将花费更少的时间阅读和理解代码,因此付出的努力将得到丰厚的回报。 如果我不必花费任何时间更改代码或添加新功能,那么花适当的时间编写代码可以节省几天的时间。 将其视为有保证的高收益投资。

因此,我的空想成为了终结。 我希望这可以帮助您编写更易于理解和维护的代码。

Source: https://habr.com/ru/post/zh-CN478760/


All Articles