PHP 中的合约编程

发布于 2024-09-29 05:09:22 字数 245 浏览 2 评论 0原文

通过契约编程是 .NET 中的现代趋势,但是 PHP 中代码契约的库/框架又如何呢?您认为这种范例对 PHP 的适用性如何?

谷歌搜索“code Contracts php”对我没有任何帮助。

注意:“按合同编写代码”,我的意思是按合同设计,所以它无关具有 .NET 或 PHP 接口。

Programming by contracts is a modern trend in .NET, but what about libraries/frameworks for code contracts in PHP? What do you think about applicability of this paradigm for PHP?

Googling for "code contracts php" gave nothing to me.

Note: by "code by contract", I mean Design by contract, so it has nothing to do with .NET or PHP interfaces.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

寄人书 2024-10-06 05:09:23

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

首先,从设计上来说,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.
}

而不是:

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() 返回抛出(维护噩梦!)。

3. 不变量:可能吗?

使用设置器,可以在属性上模拟某种代码契约。但是 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);
        }
    }
}

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

5. 结论

看看 precontracts 的实现,整个想法似乎是这样的:毫无价值。为什么我们要费心那些伪代码合约,它们实际上与普通编程语言中的代码合约有很大不同?它给我们带来了什么?除了我们可以像使用真实代码合约一样编写检查之外,几乎什么都没有。没有理由仅仅因为我们可以而这样做。

为什么普通语言中存在代码契约?有两个原因:

  • 因为它们提供了一种简单的方法来强制执行代码块开始或结束时必须匹配的条件,
  • 因为当我使用使用代码契约的 .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 方法来做事⁴,所以使用代码契约的唯一原因仍然是:因为我们可以


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

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

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

⁴一个简单的 if - throw 就可以达到目的。

I was searching for the same thing by curiosity, and found this question, so will try to give an answer.

First, PHP, by design, is not really code-contracty. You cannot even enforce, when need, the core types¹ of parameters inside the methods, so I hardly believe that code contracts will exist in PHP one day.

Let's see what happens if we do a custom, third party library/framework implementation.

1. Preconditions

The freedom of passing everything we want to a method makes code contracts (or something more or less similar to code contracts) very valuable, at least on preconditions, since protecting methods against bad values in arguments is more difficult to do, comparing to normal programming languages, where types can be enforced through the language itself.

It would be more convenient to write:

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.
}

instead of:

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. Postconditions: big problems

What is easy to do with preconditions remains impossible for postconditions. Of course, you can imagine something like:

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;
}

The only problem is that this approach has nothing to do with code contracts, neither at the implementation level (just like a preconditions example), nor on code level (since postconditions go before actual business code, not between code and method return).

It also means that if there are multiple returns in a method or a throw, postcondition will never be checked, unless you include the $this->Ensure() before every return or throw (maintenance nightmare!).

3. Invariants: possible?

With setters, it is possible to emulate some sort of code contracts on properties. But setters are so badly implemented in PHP, that this will cause too many problems, and auto-completion will not work if setters are used instead of fields.

4. Implementation

To finish, PHP is not a best candidate for code contracts, and since its design is so poor, it will probably never have code contracts, unless there will be substantial changes in future in the language design.

Currently, pseudo-code contracts² are pretty worthless when it comes to postconditions or invariants. On the other hand, some pseudo-preconditions can be easily written in PHP, making checks on arguments much more elegant and shorter.

Here's a short example of such implementation:

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);
        }
    }
}

Of course, an exception may be replaced by log-and-continue/log-and-stop approach, a error page, etc.

5. Conclusion

Looking at the implementation of precontracts, the whole idea seems worthless. Why are we bothering with those pseudo-code contracts, which are actually very different from code contracts in normal programming languages? What does it brings to us? Pretty nothing, except the fact that we can write the checks in the same way as if we were using real code contracts. And there is no reason to do this just because we can.

Why code contracts exist in normal languages? For two reasons:

  • Because they provide a simple way to enforce conditions which must be matched when a block of code starts or finishes,
  • Because when I use a .NET Framework library which uses code contracts, I can easily know within the IDE what is required by the method, and what is expected from the method, and this, without having an access to the source code³.

From what I see, in an implementation of pseudo-code contracts in PHP, the first reason is very limited, and the second one does not exist and will probably never exist.

It means that actually, a simple check of arguments is a good alternative, especially since PHP works well with arrays. Here's a copy-paste from an old personal project:

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);
                }
            }
        }
    }
}

Usage example:

/// <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;
}

It will not be enough if we want to check preconditions which are not just dependent of arguments (for example checking a value of a property in a precondition). But in most cases, all we need is to check arguments, and pseudo-code contracts in PHP are not the best way to do it.

In other words, if your only purpose is to check the arguments, pseudo-code contracts are an overkill. They may be possible when you need something more, like a precondition which depends on an object property. But in this last case, there are probably more PHPy ways to do things⁴, so the only reason to use code contracts stays: because we can.


¹ We can specify that an argument must be an instance of a class. Curiously, there is no way to specify that an argument must be an integer or a string.

² By pseudo-code contracts, I mean that the implementation presented above is very different from the implementation of code contracts in .NET Framework. The real implementation would be possible only by changing the language itself.

³ If Contract Reference Assembly is built, or, even better, if contracts are specified in an XML file.

⁴ A simple if - throw can do the trick.

尹雨沫 2024-10-06 05:09:23

我已经创建了 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 ... */

}

}

对于文档,请访问维基百科。

I have created PHP-Contract,

A lightweight and versatile implementation of C# contracts for PHP.
These contracts, in many ways, surpass the functionality in C#. Please
check out my Github project, grab a copy, and take a look at the wiki.

https://github.com/axiom82/PHP-Contract


Here is a basic example:

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 ... */

}

}

For documentation, please visit the wiki.

红焚 2024-10-06 05:09:23

接口不是contact(事实上,Laravel 定义是错误的)、Design By Contract (DbC )是一种软件正确性方法。它使用前置条件和后置条件来记录(或以编程方式断言)由程序的一部分引起的状态更改。我找到了一个很好的 php 方法这里

An interface is not a contact(in fact, Laravel definition is wrong), Design By Contract (DbC) is a software correctness methodology. It uses preconditions and postconditions to document (or programmatically assert) the change in state caused by a piece of a program. I find a good php approach here

謌踐踏愛綪 2024-10-06 05:09:23

我猜测维基百科提到了面向组件的软件方法。在此类方法中,方法被称为组件的公共接口或契约。

合同是服务提供者和客户之间的一种“协议”。在系统由不同创建者/供应商的组件组成的组件环境中,合约的“构建”至关重要。

在这种环境中,将您的组件视为一个黑匣子,它必须能够与其他人创建的其他组件有效地共存和协作,从而形成更大的系统或更大系统的子系统等

。更多细节我可以建议你谷歌搜索“组件软件 - 超越面向组件编程”一书,了解与面向组件编程相关的所有内容。

I am guessing that WikiPedia mentions Component Oriented Software Methodologies. In such methodologies methods are referred as Public Interfaces or Contracts of the component.

A contract is a 'kind-of-agreement' between the provider of the service and the client. In a component environment where systems are composed by components by various creators/vendors the 'construction' of your contracts is of critical importance.

In such environments, think of your component as a black-box that MUST be able to efficiently co-exist and collaborate with other components that other people have created, thus forming a larger system or sub-system of a larger system, etc.

For more details i can suggest you google for the 'Component Software - Beyond Component Oriented Programming' book, for all things related to component oriented programming.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文