Подтвердить что ты не робот

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

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

Googling для "code contract php" ничего не дал мне.

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

4b9b3361

Ответ 1

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

Во-первых, 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;
}

Единственная проблема заключается в том, что этот подход не имеет ничего общего с кодовыми контрактами, ни на уровне реализации (как пример preconditions), ни на уровне кода (поскольку постусловия идут до фактического бизнес-кода, а не между возвратом кода и метода).

Это также означает, что если в методе есть несколько возвратов или throw, постконфигурация никогда не будет проверяться, если вы не включите $this->Ensure() перед каждым return или throw (кошмар для обслуживания!).

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

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

Ответ 3

Я предполагаю, что WikiPedia упоминает методологии компонентного программного обеспечения. В таких методах методы называются Public Interfaces или Contracts компонента.

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

В таких средах подумайте о своем компоненте как о черном ящике, который ДОЛЖЕН быть в состоянии эффективно сосуществовать и сотрудничать с другими компонентами, создаваемыми другими людьми, создавая тем самым большую систему или подсистему более крупной системы, и др.

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

Ответ 4

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

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

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

Документация Service Container (aka IoC): https://laravel.com/docs/5.3/container