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.