Программирование по контрактам в PHP - PullRequest
12 голосов
/ 26 октября 2010

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

Поиск в Google по «php контрактов кода» мне ничего не дал.

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

Ответы [ 4 ]

25 голосов
/ 23 января 2011

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

Во-первых, 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() перед каждым 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 голосов
/ 10 марта 2014

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

Легкая и универсальная реализация контрактов C # для PHP. Эти контракты во многом превосходят функциональные возможности C #. пожалуйста посмотрите мой проект на Github, возьмите копию и посмотрите на вики.

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

}

}

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

1 голос
/ 11 октября 2018

Интерфейс не является контактом (на самом деле, Определение Laravel неверно), Design By Contract (DbC) - методология корректности программного обеспечения.Он использует предварительные условия и постусловия для документирования (или программного подтверждения) изменения состояния, вызванного частью программы.Я нашел хороший подход php здесь

0 голосов
/ 26 октября 2010

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...