PHP로 계약에 의한 프로그래밍




design-patterns code-contracts (3)

계약에 의한 프로그래밍은 .NET에서 현대적인 추세이지만 PHP의 코드 계약 라이브러리 / 프레임 워크는 어떻게 될까요? PHP에 대한이 패러다임의 적용 가능성에 대해 어떻게 생각하십니까?

"코드 계약 php"에 대한 인터넷 검색은 제게 아무 것도주지 않았습니다.

참고 : "계약에 의한 코드"는 계약에 의한 디자인을 의미하므로 .NET 또는 PHP 인터페이스와 관련이 없습니다.


나는 WikiPedia가 Component Oriented Software Methodologies에 대해 언급하고 있다고 생각합니다. 이러한 방법론에서 메서드는 공용 인터페이스 또는 구성 요소의 계약이라고합니다.

계약은 서비스 제공 업체와 고객 사이의 '일종의 합의'입니다. 시스템이 다양한 제작자 / 공급 업체의 구성 요소로 구성되는 구성 요소 환경에서 계약의 '구성'은 매우 중요합니다.

그러한 환경에서는 구성 요소를 다른 사람이 만든 다른 구성 요소와 효율적으로 공존하고 협업 할 수 있어야하는 블랙 박스로 생각하여 대형 시스템이나 대형 시스템의 하위 시스템을 형성해야합니다.

자세한 내용은 구성 요소 지향 프로그래밍과 관련된 모든 것에 대해 'Component Software - Beyond Component Oriented Programming'서적을 Google에 제안 할 수 있습니다.


저는 호기심으로 같은 것을 찾고 있었고이 질문을 발견했습니다. 그래서 대답을하려고 노력할 것입니다.

첫째, 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 에 여러 개의 반환 값이있는 경우 모든 return 또는 throw (유지 보수 악몽 $this->Ensure() 앞에 $this->Ensure() 를 포함하지 않으면 사후 조건을 검사하지 않습니다.

3. 불변량 : 가능?

setter를 사용하면 속성에 대한 일종의 코드 계약을 에뮬레이트 할 수 있습니다. 그러나 setter는 PHP에서 너무 심하게 구현되어 너무 많은 문제를 일으키고 필드 대신 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. 결론

사전 계약 이행을 살펴보면 전체 아이디어는 쓸모없는 것처럼 보입니다. 실제로 일반적인 프로그래밍 언어의 코드 계약과 매우 다른 의사 코드 계약으로 인해 귀찮게해야할까요? 그것은 우리에게 무엇을 가져다 주나요? 우리가 실제 코드 계약을 사용하는 것과 같은 방식으로 수표를 작성할 수 있다는 것을 제외하고는 거의 아무것도 아닙니다. 우리가 할 수 있기 때문에 이것을 할 이유가 없습니다.

코드 계약이 일반 언어로 된 이유는 무엇입니까? 두 가지 이유가 있습니다.

  • 코드 블록이 시작되거나 완료 될 때 일치해야하는 조건을 적용 할 수있는 간단한 방법을 제공하기 때문에,
  • 왜냐하면 코드 계약을 사용하는 .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의 의사 코드 계약은이를 수행하는 최선의 방법이 아닙니다.

즉, 유일한 목적이 인수를 확인하는 것이라면 의사 코드 계약은 과도한 것입니다. 객체 속성에 의존하는 전제 조건과 같이 더 필요한 것이 필요할 때 가능할 수 있습니다. 하지만이 마지막 경우에는 PHP를 사용하는 방법이 더 많습니다 ⁴, 코드 계약을 사용하는 유일한 이유는 다음과 같습니다.

¹ 인수가 클래스의 인스턴스 여야한다고 지정할 수 있습니다. 흥미롭게도, 인수가 정수 또는 문자열이어야한다고 지정하는 방법은 없습니다.

² 의사 코드 계약을 통해 위에서 제시 한 구현은 .NET Framework의 코드 계약 구현과 매우 다르다는 것을 의미합니다. 실제 구현은 언어 자체 만 변경하면 가능합니다.

³ Contract Reference Assembly가 구축되었거나 계약서가 XML 파일에 지정된 경우 더 좋습니다.

⁴ 간단한 if - throw 가 트릭을 할 수 있습니다.


나는 PHP 계약을 만들었고,

PHP를위한 C # 계약의 가볍고 다양한 구현. 이러한 계약은 여러면에서 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 ... */

}

}

문서 를 보려면 wiki를 방문하십시오.







design-by-contract