空值使用时若不慎,会使您的生活难以忍受,您甚至可能不了解到底是什么导致它们如此痛苦。 让我解释一下。
预设值
我们都看到了一个采用许多参数的方法,但是其中有一半以上是可选的。 结果是这样的:
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) {
此代码是唯一的。 开发人员不太可能会误解它。
值得付出努力吗?
所有这些似乎都需要额外的精力来编写执行相同操作的代码。 是的,是的。 但是与此同时,您将花费更少的时间阅读和理解代码,因此付出的努力将得到丰厚的回报。 如果我不必花费任何时间更改代码或添加新功能,那么花适当的时间编写代码可以节省几天的时间。 将其视为有保证的高收益投资。
因此,我的空想成为了终结。 我希望这可以帮助您编写更易于理解和维护的代码。