Программирование по контрактам на PHP

Программирование по контрактам-это современная тенденция в .NET, но как насчет библиотек / фреймворков для контрактов кода в PHP? Что вы думаете о применимости этой парадигмы для PHP?

Googling для "кодовых контрактов php" ничего мне не дал.

Примечание: под "кодом по контракту" я имею в виду оформление по договору, поэтому он не имеет ничего общего с .NET или PHP интерфейсами.

4 ответов


Я искал то же самое любопытство, и нашел этот вопрос, поэтому постараюсь дать ответ.

во-первых, PHP, по дизайну, на самом деле не является кодом-контрактом. Вы даже не можете применять, когда это необходимо, основные types1 параметров внутри методов, поэтому я вряд ли верю, что однажды контракты кода будут существовать в 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, postcondition никогда не будет проверяться, если вы не включите $this->Ensure() перед return или throw (кошмар техническое обслуживание!).

3. Инварианты: возможно?

с сеттерами можно эмулировать какие-то контракты кода на свойства. Но сеттеры настолько плохо реализованы в PHP, что это вызовет слишком много проблем, и автозаполнение не будет работать если вместо полей используются сеттеры.

4. Реализация

чтобы закончить, PHP не является лучшим кандидатом на контракты кода, и поскольку его дизайн настолько плох, он, вероятно, никогда не будет иметь контрактов кода, если в будущем не будут существенные изменения в дизайне языка.

в настоящее время, псевдо-код contracts2 довольно бесполезны, когда дело доходит до постусловий и инвариантов. С другой стороны, некоторые псевдо-предварительные условия можно легко записать в 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. Вывод

глядя на реализацию предварительных условий, вся идея кажется бесполезной. Почему мы беспокоимся о тех псевдо-кодовых контрактах, которые на самом деле очень отличаются от кода контракты на обычных языках программирования? Что это нам дает? Почти ничего, кроме того, что мы можем выписывать чеки так же, как если бы мы использовали реальные кодовые контракты. И нет никаких причин делать это просто потому что мы можем.

почему контракты кода существуют на обычных языках? По двум причинам:--12-->

  • потому что они обеспечивают простой способ принудительного выполнения условий, которые должны быть сопоставлены при запуске блока кода или заканчивает,
  • потому что, когда я использую библиотеку .NET Framework, которая использует контракты кода, я могу легко узнать в IDE, что требуется методом, и что ожидается от метода, и это, не имея доступа к исходному коду3.

из того, что я вижу, в реализации псевдокодовых контрактов в 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 мы можем указать, что аргумент должен быть экземпляром класса. Любопытно, что невозможно указать, что аргумент должен быть целым числом или строкой.

2 под псевдо-кодовыми контрактами я имею в виду, что реализация, представленная выше, сильно отличается от реализации кодовых контрактов в .NET Framework. Реальная реализация будет возможна только путем изменения самого языка.

3 если сборка ссылки контракта построена, или, еще лучше, если контракты указаны в XML файл.

simple простой if - throw может сделать трюк.


Я создал PHP-контракт,

легкая и универсальная реализация контрактов C# для PHP. Эти контракты во многом превосходят функциональность 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 ... */

}

}

для документация,пожалуйста, посетите Вики.


Я предполагаю, что Википедия упоминает компонентно-ориентированные программные методологии. В таких методологиях методы называются открытыми интерфейсами или контрактами компонента.

договор является "своего рода соглашением" между поставщиком услуги и клиентом. В компонентной среде, где системы состоят из компонентов различными создателями / поставщиками, "построение" ваших контрактов имеет решающее значение.

в таких условиях, думаю вашего компонента как черного ящика, который должен быть способен эффективно сосуществовать и сотрудничать с другими компонентами, созданными другими людьми, формируя тем самым большую систему или подсистему большей системы и т. д.

для более подробной информации я могу предложить Вам google для книги "компонентное программное обеспечение-за пределами компонентного ориентированного программирования", для всех вещей, связанных с компонентным ориентированным программированием.


Laravel PHP Framework использует контракты изначально для своих реализаций.

на этой платформе вы можете реализовать абстрактные интерфейсы (контракты) и связать их с конкретной реализацией "на лету" благодаря мощной системе впрыска зависимостей (IoC).

контракты философия на Laravel:https://laravel.com/docs/5.3/contracts

сервисный контейнер (он же IoC) документация : https://laravel.com/docs/5.3/container