Parametertyp-Kovarianz in Spezialisierungen
Welche Strategien gibt es, um die Parametertypinvarianz für Spezialisierungen in einer Sprache zu überwinden (PHP) ohne Unterstützung für Generika?
Anmerkung: Ich wünschte, ich könnte sagen, mein Verständnis der Typentheorie / Sicherheit / Varianz / etc. Wäre vollständiger. Ich bin kein CS-Major.
SituationDu hast eine abstrakte Klasse,Consumer
, die Sie erweitern möchten.Consumer
deklariert eine abstrakte Methodeconsume(Argument $argument)
das braucht eine Definition. Sollte kein Problem sein.
Ihr SpezialistConsumer
, namensSpecializedConsumer
hat kein logisches Geschäft mitjeden Art derArgument
. Stattdessen sollte es ein akzeptierenSpecializedArgument
(und Unterklassen davon). Unsere Methodensignatur ändert sich zuconsume(SpecializedArgument $argument)
.
abstract class Argument { }
class SpecializedArgument extends Argument { }
abstract class Consumer {
abstract public function consume(Argument $argument);
}
class SpecializedConsumer extends Consumer {
public function consume(SpecializedArgument $argument) {
// i dun goofed.
}
}
Wir brechenLiskov-Substitutionsprinzipund Typ Sicherheitsprobleme verursachen. Poop.
FrageOk, das wird nicht funktionieren. In Anbetracht dieser Situation, welche Muster oder Strategien existieren, um das Typensicherheitsproblem und die Verletzung von zu überwindenLSP, aber immer noch die Typbeziehung vonSpecializedConsumer
zuConsumer
?
Ich nehme an, es ist vollkommen akzeptabel, dass eine Antwort auf "Du bist bescheuert, zurück zum Zeichenbrett".
Überlegungen, Details und ErrataIn Ordnung, eine sofortige Lösung präsentiert sich als "definiere das nichtconsume()
Methode inConsumer
". Ok, das macht Sinn, weil die Methodendeklaration nur so gut ist wie die Signaturconsume()
Selbst mit einer unbekannten Parameterliste tut mein Gehirn ein bisschen weh. Vielleicht gibt es einen besseren Weg.
Nach dem, was ich lese, unterstützen nur wenige Sprachen die Parametertyp-Kovarianz. PHP ist eine davon und hier die Implementierungssprache. Weitere Komplikationen habe ich kreativ gesehen "lösungen"beteiligtGenerika; eine weitere Funktion, die in PHP nicht unterstützt wird.
Aus WikisVarianz (Informatik) - Benötigen Sie kovariante Argumenttypen?:
Dies führt in einigen Situationen zu Problemen, in denen Argumenttypen kovariant sein sollten, um die tatsächlichen Anforderungen zu modellieren. Angenommen, Sie haben eine Klasse, die eine Person repräsentiert. Eine Person kann den Arzt aufsuchen, so dass diese Klasse möglicherweise eine Methode für nichtig erklärtPerson::see(Doctor d)
. Angenommen, Sie möchten eine Unterklasse vonPerson
Klasse,Child
. Das ist einChild
Ist eine Person. Man könnte dann gerne eine Unterklasse daraus machenDoctor
, Pediatrician
. Wenn Kinder nur Kinderärzte aufsuchen, möchten wir dies im Typensystem durchsetzen. Eine naive Implementierung schlägt jedoch fehl: weil aChild
ist einPerson
, Child::see(d)
muss keine nehmenDoctor
, nicht nur einPediatrician
.
In dem Artikel heißt es weiter:
In diesem Fall ist dieBesuchermuster könnte verwendet werden, um diese Beziehung durchzusetzen. Eine andere Möglichkeit, die Probleme in C ++ zu lösen, ist die Verwendung vongenerische Programmierung.
Nochmal,Generika kann kreativ eingesetzt werden, um das Problem zu lösen. Ich erkunde dieBesuchermusterDa ich sowieso eine halbherzige Implementierung habe, nutzen die meisten Implementierungen, wie in den Artikeln beschrieben, das Überladen von Methoden, ein weiteres nicht unterstütztes Feature in PHP.
<too-much-information>
Aufgrund der jüngsten Diskussion werde ich auf die spezifischen Implementierungsdetails eingehen, die ich versäumt habe (wie in, ich werde wahrscheinlich viel zu viel einschließen).
Der Kürze halber habe ich Methodenkörper für diejenigen ausgeschlossen, die (sollte sein) in ihrem Zweck überdeutlich. Ich habeversucht Um es kurz zu machen, aber ich neige dazu, wortreich zu werden. Ich wollte keine Code-Wand wegwerfen, daher folgen / gehen Erklärungen den Code-Blöcken voran. Wenn Sie Bearbeitungsrechte haben und dies bereinigen möchten, tun Sie dies bitte. Außerdem sind Codeblöcke keine Copy-Pasta aus einem Projekt. Wenn etwas keinen Sinn ergibt, kann es auch anders sein. schreie mich zur Klarstellung an.
In Bezug auf die ursprüngliche Frage, im Folgenden dieRule
Klasse ist dieConsumer
und dasAdapter
Klasse ist dieArgument
.
Die baumbezogenen Klassen setzen sich wie folgt zusammen:
abstract class Rule {
abstract public function evaluate(Adapter $adapter);
abstract public function getAdapter(Wrapper $wrapper);
}
abstract class Node {
protected $rules = [];
protected $command;
public function __construct(array $rules, $command) {
$this->addEachRule($rules);
}
public function addRule(Rule $rule) { }
public function addEachRule(array $rules) { }
public function setCommand(Command $command) { }
public function evaluateEachRule(Wrapper $wrapper) {
// see below
}
abstract public function evaluate(Wrapper $wrapper);
}
class InnerNode extends Node {
protected $nodes = [];
public function __construct(array $rules, $command, array $nodes) {
parent::__construct($rules, $command);
$this->addEachNode($nodes);
}
public function addNode(Node $node) { }
public function addEachNode(array $nodes) { }
public function evaluateEachNode(Wrapper $wrapper) {
// see below
}
public function evaluate(Wrapper $wrapper) {
// see below
}
}
class OuterNode extends Node {
public function evaluate(Wrapper $wrapper) {
// see below
}
}
Also jederInnerNode
enthältRule
undNode
Objekte und jedesOuterNode
nurRule
Objekte.Node::evaluate()
bewertet jeweilsRule
(Node::evaluateEachRule()
) zu einem Booleschentrue
. Wenn jederRule
vergeht, dieNode
ist vergangen und es istCommand
wird dem hinzugefügtWrapper
, und wird zur Auswertung an Kinder herabsteigen (OuterNode::evaluateEachNode()
) oder einfach zurücktrue
, zumInnerNode
undOuterNode
Objekte jeweils.
Wie fürWrapper
; dasWrapper
Objekt-Proxies aRequest
Objekt und hat eine Sammlung vonAdapter
Objekte. DasRequest
Objekt ist eine Darstellung der HTTP-Anforderung. DasAdapter
Objekt ist eine spezialisierte Schnittstelle (und behält spezifischen Zustand bei) für bestimmte Verwendung mit bestimmtenRule
Objekte. (Hier kommen die LSP-Probleme ins Spiel)
DasCommand
Objekt ist eine Aktion (ein ordentlich verpackter Rückruf, wirklich), der dem. hinzugefügt wirdWrapper
Objekt, wenn alles gesagt und getan ist, das Array vonCommand
Objekte werden nacheinander abgefeuert und passieren dieRequest
(unter anderem) im.
class Request {
// all teh codez for HTTP stuffs
}
class Wrapper {
protected $request;
protected $commands = [];
protected $adapters = [];
public function __construct(Request $request) {
$this->request = $request;
}
public function addCommand(Command $command) { }
public function getEachCommand() { }
public function adapt(Rule $rule) {
$type = get_class($rule);
return isset($this->adapters[$type])
? $this->adapters[$type]
: $this->adapters[$type] = $rule->getAdapter($this);
}
public function commit(){
foreach($this->adapters as $adapter) {
$adapter->commit($this->request);
}
}
}
abstract class Adapter {
protected $wrapper;
public function __construct(Wrapper $wrapper) {
$this->wrapper = $wrapper;
}
abstract public function commit(Request $request);
}
Also ein gegebenes User-LandRule
Akzeptiert das erwartete User-LandAdapter
. Wenn dasAdapter
Informationen über die Anforderung benötigt, wird sie weitergeleitetWrapper
, um die Unversehrtheit des Originals zu erhaltenRequest
.
Als dieWrapper
AggregateAdapter
Objekte werden vorhandene Instanzen an nachfolgende übergebenRule
Objekte, so dass der Zustand einesAdapter
ist von einem erhaltenRule
zum nächsten. EinmalEin Ganzes baum ist gegangen,Wrapper::commit()
wird aufgerufen, und jeder der aggregiertenAdapter
Objekte wenden den erforderlichen Status auf das Original anRequest
.
Wir werden dann mit einer Reihe von verlassenCommand
Objekte und eine modifizierteRequest
.
Was zum Teufel ist der Punkt?
Nun, ich wollte die prototypische "Routing-Tabelle", die in vielen PHP-Frameworks / -Anwendungen üblich ist, nicht neu erstellen, also entschied ich mich stattdessen für einen "Routing-Baum". Indem Sie beliebige Regeln zulassen, können Sie schnell eine erstellen und anfügenAuthRule
(zum Beispiel) zu einemNode
, und nicht länger ist diese ganze Filiale zugänglich, ohne dieAuthRule
. In der Theorie (in meinem Kopf) Es ist wie ein magisches Einhorn, das die Vervielfältigung von Code verhindert und die Organisation von Zonen / Modulen erzwingt. In der Praxis bin ich verwirrt und habe Angst.
Warum habe ich diese Unsinnsmauer verlassen?
Nun, dies ist die Implementierung, für die ich das LSP-Problem beheben muss. JederRule
entspricht einemAdapter
und das ist nicht gut Ich möchte die Beziehung zwischen beiden bewahrenRule
, um die Typensicherheit beim Erstellen des Baums usw. zu gewährleisten, kann ich die Schlüsselmethode jedoch nicht deklarieren (evaluate()
) in der ZusammenfassungRule
, wenn sich die Signatur für Untertypen ändert.
In einem weiteren Punkt arbeite ich daran, das Problem zu lösenAdapter
Erstellungs- / Verwaltungsschema; ob es in der Verantwortung derRule
um es zu schaffen, etc.
</too-much-information>