Parametertyp-Kovarianz in Spezialisierungen

tl; dr

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.

Situation

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

Problem

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.

Frage

Ok, 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 Errata

In 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>

Implementierung

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 einemAdapterund 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>

Antworten auf die Frage(2)

Ihre Antwort auf die Frage