用PHP编写合同编程

zer*_*kms 12 php design-patterns design-by-contract code-contracts

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

谷歌搜索"代码合同PHP"没有给我任何帮助.

注意:"按合同编写代码",我指的是按合同设计,因此它与.NET或PHP接口无关.

Ars*_*nko 25

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

首先,PHP在设计上并不是真正的代码契约.在需要时,你甚至无法强制执行方法中的核心类型参数,所以我很难相信代码合同将在PHP中存在一天.

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

1.先决条件

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

写起来会更方便:

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.
}
Run Code Online (Sandbox Code Playgroud)

代替:

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.
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

不变量:可能吗?

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

4.实施

为了完成,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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,可以通过log-and-continue/log-and-stop方法,错误页面等替换异常.

5.结论

看一下预收缩的实施,整个想法似乎毫无价值.为什么我们要困扰那些与普通编程语言中的代码契约实际上非常不同的伪代码契约呢?它给我们带来了什么?什么都没有,除了我们可以像使用真正的代码合同一样编写支票的事实.没有理由这样做只是因为我们可以.

为什么代码合同以普通语言存在?有两个原因:

  • 因为它们提供了一种简单的方法来强制执行在代码块开始或结束时必须匹配的条件,
  • 因为当我使用一个使用代码契约的.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);
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法示例:

/// <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;
}
Run Code Online (Sandbox Code Playgroud)

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

换句话说,如果您的唯一目的是检查参数,那么伪代码合约就是一种过度杀伤力.当你需要更多东西时,它们可能是可能的,比如依赖于对象属性的前提条件.但在最后一种情况下,可能有更多PHPy方法可以做事情,所以使用代码契约的唯一原因仍然是:因为我们可以.


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

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

³如果构建了Contract Reference Assembly,或者甚至更好,如果在XML文件中指定了合同.

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

  • 游戏在PHP7中发生了变化.现在,您可以为函数/方法参数指定类型并返回.但是,当涉及抽象类/接口时,现在出现了覆盖方法参数的异常,但PHP团队正在努力解决这个问题. (2认同)