15 Stimmen

Programmierung nach Verträgen in PHP

Programmierung durch Verträge ist ein moderner Trend in .NET, aber was ist mit Bibliotheken/Frameworks für Codeverträge in PHP? Was denken Sie über die Anwendbarkeit dieses Paradigmas für PHP?

Wenn ich nach "code contracts php" googel, finde ich nichts.

Anmerkung: Mit "Code by contract" meine ich Entwurf im Auftrag Es hat also nichts mit .NET- oder PHP-Schnittstellen zu tun.

28voto

Arseni Mourzenko Punkte 47813

Ich habe aus Neugier nach der gleichen Sache gesucht und diese Frage gefunden, also werde ich versuchen, eine Antwort zu geben.

Erstens ist PHP von seiner Konzeption her nicht wirklich auf Code-Verträge ausgelegt. Man kann nicht einmal die Kerntypen¹ von Parametern innerhalb der Methoden erzwingen, daher glaube ich kaum, dass es eines Tages Codeverträge in PHP geben wird.

Schauen wir uns an, was passiert, wenn wir eine benutzerdefinierte Bibliothek/Framework eines Drittanbieters implementieren.

1. Voraussetzungen

Die Freiheit, einer Methode alles zu übergeben, was wir wollen, macht Code-Verträge (oder etwas mehr oder weniger Ähnliches wie Code-Verträge) sehr wertvoll, zumindest bei Vorbedingungen, da der Schutz von Methoden gegen schlechte Werte in Argumenten schwieriger zu bewerkstelligen ist, als bei normalen Programmiersprachen, wo Typen durch die Sprache selbst erzwungen werden können.

Es wäre bequemer, zu schreiben:

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

anstelle von:

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. Postkonditionen: große Probleme

Was bei Vorbedingungen leicht möglich ist, bleibt bei Nachbedingungen unmöglich. Natürlich kann man sich etwas vorstellen wie:

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

Das einzige Problem ist, dass dieser Ansatz nichts mit Code-Verträgen zu tun hat, weder auf der Implementierungsebene (genau wie das Beispiel mit den Vorbedingungen) noch auf der Code-Ebene (da die Nachbedingungen vor dem eigentlichen Geschäftscode stehen, nicht zwischen Code und Methodenrückgabe).

Das bedeutet auch, dass, wenn es mehrere Rückgaben in einer Methode oder einem throw wird die Nachbedingung nie überprüft, es sei denn, Sie schließen die $this->Ensure() vor jedem return o throw (ein Alptraum für die Wartung!).

3. Invarianten: möglich?

Mit Settern ist es möglich, eine Art von Codeverträgen für Eigenschaften zu emulieren. Aber Setter sind in PHP so schlecht implementiert, dass dies zu viele Probleme verursachen wird, und die Autovervollständigung wird nicht funktionieren, wenn Setter anstelle von Feldern verwendet werden.

4. Umsetzung

Abschließend lässt sich sagen, dass PHP nicht der beste Kandidat für Code-Verträge ist, und da sein Design so schlecht ist, wird es wahrscheinlich nie Code-Verträge geben, es sei denn, es wird in Zukunft wesentliche Änderungen am Design der Sprache geben.

Derzeit sind Pseudocode-Verträge² ziemlich wertlos, wenn es um Postconditions oder Invarianten geht. Andererseits können einige Pseudovoraussetzungen leicht in PHP geschrieben werden, wodurch die Überprüfung von Argumenten viel eleganter und kürzer wird.

Hier ist ein kurzes Beispiel für eine solche Implementierung:

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

Natürlich kann eine Ausnahme durch einen Log-and-Continue- bzw. Log-and-Stop-Ansatz, eine Fehlerseite usw. ersetzt werden.

5. Schlussfolgerung

Wenn man sich die Umsetzung von Vorverträgen ansieht, scheint die ganze Idee wertlos zu sein. Warum machen wir uns die Mühe mit diesen Pseudocode-Verträgen, die sich von den Code-Verträgen in normalen Programmiersprachen doch sehr unterscheiden? Was bringen sie uns? So ziemlich nichts, außer der Tatsache, dass wir die Prüfungen genauso schreiben können, als würden wir echte Codeverträge verwenden. Und es gibt keinen Grund, dies nur zu tun denn wir können .

Warum gibt es Codeverträge in normalen Sprachen? Aus zwei Gründen:

  • Denn sie bieten eine einfache Möglichkeit, Bedingungen zu erzwingen, die erfüllt sein müssen, wenn ein Codeblock beginnt oder beendet wird,
  • Denn wenn ich eine .NET Framework-Bibliothek verwende, die Code-Verträge verwendet, kann ich in der IDE leicht wissen, was von der Methode benötigt wird und was von der Methode erwartet wird, und das, ohne Zugang zum Quellcode zu haben.

Soweit ich sehe, ist der erste Grund bei einer Implementierung von Pseudocode-Verträgen in PHP sehr begrenzt, und der zweite Grund existiert nicht und wird wahrscheinlich nie existieren.

Das bedeutet, dass eine einfache Überprüfung der Argumente eine gute Alternative ist, zumal PHP gut mit Arrays arbeitet. Hier ist ein Copy-Paste aus einem alten persönlichen Projekt:

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

Beispiel für die Verwendung:

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

Es reicht nicht aus, wenn wir Vorbedingungen prüfen wollen, die nicht nur von Argumenten abhängig sind (z.B. Prüfung des Wertes einer Eigenschaft in einer Vorbedingung). Aber in den meisten Fällen müssen wir nur die Argumente prüfen, und Pseudocode-Verträge in PHP sind nicht der beste Weg, dies zu tun.

Mit anderen Worten: Wenn Ihr einziger Zweck darin besteht, die Argumente zu überprüfen, sind Pseudocode-Verträge ein Overkill. Sie können möglich sein, wenn Sie etwas mehr brauchen, wie eine Vorbedingung, die von einer Objekteigenschaft abhängt. Aber in diesem letzten Fall gibt es wahrscheinlich mehr PHPy Möglichkeiten, Dinge zu tun, so dass der einzige Grund, Code-Verträge zu verwenden bleibt: denn wir können .


¹ Wir können festlegen, dass ein Argument eine Instanz einer Klasse sein muss. Seltsamerweise gibt es keine Möglichkeit, anzugeben, dass ein Argument eine ganze Zahl oder eine Zeichenkette sein muss.

² Mit Pseudocode-Verträgen meine ich, dass die oben vorgestellte Implementierung sich stark von der Implementierung von Code-Verträgen in .NET Framework unterscheidet. Die echte Implementierung wäre nur durch eine Änderung der Sprache selbst möglich.

Wenn Contract Reference Assembly erstellt wird, oder noch besser, wenn Verträge in einer XML-Datei angegeben werden.

Eine einfache if - throw können den Zweck erfüllen.

2voto

axiom82 Punkte 622

Ich habe PHP-Contract erstellt,

A Diese Verträge, in vielerlei Hinsicht, übertreffen die Funktionalität in C #. Bitte besuchen Sie mein Github-Projekt, holen Sie sich eine Kopie und werfen Sie einen Blick in das Wiki.

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


Hier ist ein einfaches Beispiel:

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

}

}

Für die Dokumentation, besuchen Sie bitte das Wiki.

1voto

En Schnittstelle ist kein Kontakt (in der Tat, Laravel-Definition falsch ist), ist Design By Contract (DbC) eine Softwarekorrektheitsmethodik. Sie verwendet Vorbedingungen und Nachbedingungen, um die durch einen Programmteil verursachte Zustandsänderung zu dokumentieren (oder programmatisch zu bestätigen). Ich finde einen guten php-Ansatz aquí

0voto

Andreas Punkte 5247

Ich vermute, dass WikiPedia komponentenorientierte Software-Methoden erwähnt. In solchen Methodologien werden Methoden als öffentliche Schnittstellen oder Verträge der Komponente bezeichnet.

Ein Vertrag ist eine Art Vereinbarung zwischen dem Anbieter der Dienstleistung und dem Kunden. In einer Komponentenumgebung, in der Systeme aus Komponenten verschiedener Hersteller/Anbieter zusammengesetzt sind, ist die "Konstruktion" Ihrer Verträge von entscheidender Bedeutung.

In solchen Umgebungen sollten Sie Ihre Komponente als eine Blackbox betrachten, die in der Lage sein MUSS, mit anderen Komponenten, die von anderen erstellt wurden, effizient zu koexistieren und zusammenzuarbeiten und so ein größeres System oder ein Teilsystem eines größeren Systems usw. zu bilden.

Für weitere Details kann ich Ihnen vorschlagen, Google für die 'Component Software - Beyond Component Oriented Programming' Buch, für alle Dinge im Zusammenhang mit komponentenorientierten Programmierung.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X