合约编程是 .NET 的现代趋势,但是 PHP 中代码合约的库/框架呢?您如何看待这种范式对 PHP 的适用性?
谷歌搜索“代码合同 php”对我没有任何帮助。
注意:“契约式代码”是指契约式设计,因此它与 .NET 或 PHP 接口无关。
合约编程是 .NET 的现代趋势,但是 PHP 中代码合约的库/框架呢?您如何看待这种范式对 PHP 的适用性?
谷歌搜索“代码合同 php”对我没有任何帮助。
注意:“契约式代码”是指契约式设计,因此它与 .NET 或 PHP 接口无关。
我好奇地寻找同样的东西,发现了这个问题,所以会尝试给出答案。
首先,PHP 在设计上并不是真正的代码契约。您甚至无法在需要时强制执行方法中参数的核心类型¹,所以我几乎不相信代码合同有一天会存在于 PHP 中。
让我们看看如果我们进行自定义的第三方库/框架实现会发生什么。
将我们想要的所有内容传递给方法的自由使得代码契约(或或多或少类似于代码契约的东西)非常有价值,至少在前提条件下是这样,因为与普通编程相比,保护方法免受参数中的错误值的影响更加困难语言,其中类型可以通过语言本身强制执行。
写起来会更方便:
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.
}
用前置条件容易做的事情,对于后置条件来说仍然是不可能的。当然,你可以想象这样的事情:
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()
之前的每个return
或throw
(维护噩梦!)。
使用 setter,可以在属性上模拟某种代码协定。但是 setter 在 PHP 中的实现非常糟糕,这会导致太多的问题,如果使用 setter 而不是字段,自动完成将不起作用。
最后,PHP 并不是代码契约的最佳候选者,而且由于它的设计如此糟糕,它可能永远不会有代码契约,除非未来语言设计会有重大变化。
目前,伪代码合约²在后置条件或不变量方面毫无价值。另一方面,一些伪前提条件可以很容易地用 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 方法、错误页面等替换。
看看预合同的实施,整个想法似乎毫无价值。为什么我们要为那些伪代码合约而烦恼,它们实际上与普通编程语言中的代码合约非常不同?它给我们带来了什么?几乎什么都没有,除了我们可以像使用真实代码合约一样编写检查。没有理由仅仅因为我们可以这样做。
为什么代码合约以普通语言存在?有两个原因:
据我所知,在 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 中代码契约的实现有很大不同。只有改变语言本身才能真正实现。
³ 如果构建了合同参考程序集,或者更好的是,如果合同在 XML 文件中指定。
⁴ 一个简单if - throw
的就可以了。
我创建了 PHP 合同,
PHP 的 C# 合同的轻量级和多功能实现。这些契约在很多方面都超越了 C# 中的功能。请查看我的 Github 项目,获取一份副本,然后查看 wiki。
https://github.com/axiom82/PHP-Contract
这是一个基本示例:
class Model {
public function getFoos($barId, $includeBaz = false, $limit = 0, $offset = 0){
$contract = new Contract();
$contract->term('barId')->id()->end()
->term('includeBaz')->boolean()->end()
->term('limit')->natural()->end()
->term('offset')->natural()->end()
->metOrThrow();
/* Continue with peace of mind ... */
}
}
有关文档,请访问 wiki。
我猜维基百科提到了面向组件的软件方法。在此类方法中,方法被称为组件的公共接口或合同。
合同是服务提供者和客户之间的一种“协议”。在组件环境中,系统由各种创建者/供应商的组件组成,合同的“构建”至关重要。
在这样的环境中,将您的组件视为一个黑盒,它必须能够与其他人创建的其他组件有效地共存和协作,从而形成更大的系统或更大系统的子系统等。
有关更多详细信息,我建议您在谷歌上搜索“组件软件 - 超越面向组件的编程”一书,了解与面向组件编程相关的所有内容。