gpt4 book ai didi

php - PHP 中的契约式编程

转载 作者:行者123 更新时间:2023-12-03 00:44:49 25 4
gpt4 key购买 nike

通过契约编程是 .NET 中的一个现代趋势,但是 PHP 中代码契约的库/框架呢?您如何看待这种范式对 PHP 的适用性?

谷歌搜索“代码契约(Contract) php”对我没有任何帮助。

注意:通过“合约代码”,我的意思是 Design by contract ,所以它与 .NET 或 PHP 接口(interface)无关。

最佳答案

我好奇地寻找同样的东西,发现了这个问题,所以会尝试给出答案。

首先,按照设计,PHP 并不是真正的代码契约。您甚至无法在需要时强制执行方法内部参数的核心类型¹,因此我几乎不相信代码契约会在某一天出现在 PHP 中。

让我们看看如果我们进行自定义的第三方库/框架实现会发生什么。

1. 前提条件

将我们想要的一切传递给方法的自由使得代码契约(或或多或少类似于代码契约的东西)非常有值(value),至少在前提条件下,因为与普通编程相比,保护方法免受参数中的错误值的影响更难做到语言,其中类型可以通过语言本身强制执行。

写成这样会更方便:

public function AddProduct($productId, $name, $price, $isCurrentlyInStock)
{
Contracts::Require(__FILE__, __LINE__, is_int($productId), 'The product ID must be an integer.');
Contracts::Require(__FILE__, __LINE__, is_string($name), 'The product name must be a string.');
Contracts::Require(__FILE__, __LINE__, is_int($price), 'The price must be an integer.');
Contracts::Require(__FILE__, __LINE__, is_bool($isCurrentlyInStock), 'The product availability must be an boolean.');

Contracts::Require(__FILE__, __LINE__, $productId > 0 && $productId <= 5873, 'The product ID is out of range.');
Contracts::Require(__FILE__, __LINE__, $price > 0, 'The product price cannot be negative.');

// Business code goes here.
}

代替:
public function AddProduct($productId, $name, $price, $isCurrentlyInStock)
{
if (!is_int($productId))
{
throw new ArgumentException(__FILE__, __LINE__, 'The product ID must be an integer.');
}

if (!is_int($name))
{
throw new ArgumentException(__FILE__, __LINE__, 'The product name must be a string.');
}

// Continue with four other checks.

// Business code goes here.
}

2.后置条件:大问题

对先决条件来说很容易做到的事情对于后置条件来说仍然是不可能的。当然,你可以想象这样的事情:
public function FindLastProduct()
{
$lastProduct = ...

// Business code goes here.

Contracts::Ensure($lastProduct instanceof Product, 'The method was about to return a non-product, when an instance of a Product class was expected.');
return $lastProduct;
}

唯一的问题是这种方法与代码契约无关,无论是在实现级别(就像前置条件示例一样),还是在代码级别(因为后置条件在实际业务代码之前,而不是在代码和方法返回之间)。

这也意味着如果一个方法或一个 throw 中有多个返回值, 永远不会检查后置条件,除非您包含 $this->Ensure()每次前 returnthrow (维护噩梦!)。

3. 不变量:可能吗?

使用 setter,可以在属性上模拟某种代码契约。但是在 PHP 中 setter 的实现非常糟糕,这会导致太多问题,如果使用 setter 而不是字段,则自动完成将不起作用。

4. 实现

最后,PHP 不是代码契约的最佳候选者,而且由于它的设计如此糟糕,它可能永远不会有代码契约,除非 future 语言设计会有实质性的变化。

目前,当涉及到后置条件或不变量时,伪代码契约² 毫无值(value)。另一方面,一些伪前置条件可以很容易地用 PHP 编写,从而使对参数的检查更加优雅和简短。

这是此类实现的一个简短示例:
class ArgumentException extends Exception
{
// Code here.
}

class CodeContracts
{
public static function Require($file, $line, $precondition, $failureMessage)
{
Contracts::Require(__FILE__, __LINE__, is_string($file), 'The source file name must be a string.');
Contracts::Require(__FILE__, __LINE__, is_int($line), 'The source file line must be an integer.');
Contracts::Require(__FILE__, __LINE__, is_string($precondition), 'The precondition must evaluate to a boolean.');
Contracts::Require(__FILE__, __LINE__, is_int($failureMessage), 'The failure message must be a string.');

Contracts::Require(__FILE__, __LINE__, $file != '', 'The source file name cannot be an empty string.');
Contracts::Require(__FILE__, __LINE__, $line >= 0, 'The source file line cannot be negative.');

if (!$precondition)
{
throw new ContractException('The code contract was violated in ' . $file . ':' . $line . ': ' . $failureMessage);
}
}
}

当然,异常可能会被 log-and-continue/log-and-stop 方法、错误页面等替代。

5. 结论

看看预契约(Contract)的实现,整个想法似乎毫无值(value)。为什么我们要为那些伪代码合约而烦恼,它们实际上与普通编程语言中的代码合约非常不同?它给我们带来了什么?几乎没有,除了我们可以像使用真正的代码契约(Contract)一样编写检查的事实。没有理由仅仅因为我们可以这样做。

为什么代码契约存在于普通语言中?有两个原因:
  • 因为它们提供了一种简单的方法来强制执行在代码块开始或结束时必须匹配的条件,
  • 因为当我使用使用代码契约的 .NET Framework 库时,我可以在 IDE 中轻松了解该方法需要什么,以及该方法的预期内容,而无需访问源代码³。

  • 据我了解,在 PHP 中实现伪代码契约时,第一个原因非常有限,第二个原因不存在,可能永远不会存在。

    这意味着实际上,简单的参数检查是一个不错的选择,特别是因为 PHP 可以很好地处理数组。这是一个旧的个人项目的复制粘贴:
    class ArgumentException extends Exception
    {
    private $argumentName = null;

    public function __construct($message = '', $code = 0, $argumentName = '')
    {
    if (!is_string($message)) throw new ArgumentException('Wrong parameter for ArgumentException constructor. String value expected.', 0, 'message');
    if (!is_long($code)) throw new ArgumentException('Wrong parameter for ArgumentException constructor. Integer value expected.', 0, 'code');
    if (!is_string($argumentName)) throw new ArgumentException('Wrong parameter for ArgumentException constructor. String value expected.', 0, 'argumentName');
    parent::__construct($message, $code);
    $this->argumentName = $argumentName;
    }

    public function __toString()
    {
    return 'exception \'' . get_class($this) . '\' ' . ((!$this->argumentName) ? '' : 'on argument \'' . $this->argumentName . '\' ') . 'with message \'' . parent::getMessage() . '\' in ' . parent::getFile() . ':' . parent::getLine() . '
    Stack trace:
    ' . parent::getTraceAsString();
    }
    }

    class Component
    {
    public static function CheckArguments($file, $line, $args)
    {
    foreach ($args as $argName => $argAttributes)
    {
    if (isset($argAttributes['type']) && (!VarTypes::MatchType($argAttributes['value'], $argAttributes['type'])))
    {
    throw new ArgumentException(String::Format('Invalid type for argument \'{0}\' in {1}:{2}. Expected type: {3}.', $argName, $file, $line, $argAttributes['type']), 0, $argName);
    }
    if (isset($argAttributes['length']))
    {
    settype($argAttributes['length'], 'integer');
    if (is_string($argAttributes['value']))
    {
    if (strlen($argAttributes['value']) != $argAttributes['length'])
    {
    throw new ArgumentException(String::Format('Invalid length for argument \'{0}\' in {1}:{2}. Expected length: {3}. Current length: {4}.', $argName, $file, $line, $argAttributes['length'], strlen($argAttributes['value'])), 0, $argName);
    }
    }
    else
    {
    throw new ArgumentException(String::Format('Invalid attributes for argument \'{0}\' in {1}:{2}. Either remove length attribute or pass a string.', $argName, $file, $line), 0, $argName);
    }
    }
    }
    }
    }

    用法示例:
    /// <summary>
    /// Determines whether the ending of the string matches the specified string.
    /// </summary>
    public static function EndsWith($string, $end, $case = true)
    {
    Component::CheckArguments(__FILE__, __LINE__, array(
    'string' => array('value' => $string, 'type' => VTYPE_STRING),
    'end' => array('value' => $end, 'type' => VTYPE_STRING),
    'case' => array('value' => $case, 'type' => VTYPE_BOOL)
    ));

    $stringLength = strlen($string);
    $endLength = strlen($end);
    if ($endLength > $stringLength) return false;
    if ($endLength == $stringLength && $string != $end) return false;

    return (($case) ? substr_compare($string, $end, $stringLength - $endLength) : substr_compare($string, $end, $stringLength - $endLength, $stringLength, true)) == 0;
    }

    如果我们想要检查不只是依赖于参数的先决条件(例如检查先决条件中属性的值),这还不够。但在大多数情况下,我们所需要的只是检查参数,而 PHP 中的伪代码契约并不是最好的方法。

    换句话说,如果您的唯一目的是检查参数,那么伪代码合约是一种矫枉过正。当您需要更多东西时,它们可能是可能的,例如取决于对象属性的先决条件。但在最后一种情况下,可能有更多的 PHPy 方式来做事⁴,所以使用代码契约的唯一原因仍然是:因为我们可以。

    ¹ 我们可以指定一个参数必须是一个类的实例。奇怪的是,没有办法指定参数必须是整数或字符串。

    ² 通过伪代码契约,我的意思是上面介绍的实现与 .NET Framework 中代码契约的实现有很大的不同。真正的实现只有通过改变语言本身才有可能。

    ³ 如果构建了契约(Contract)引用程序集,或者更好的是,如果在 XML 文件中指定了契约(Contract)。

    ⁴ 一个简单的 if - throw可以做到这一点。

    关于php - PHP 中的契约式编程,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4019748/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com