Kowariancja typu parametru w specjalizacjach

tl; dr

Jakie są strategie pokonywania niezmienności typów parametrów dla specjalizacji w języku (PHP) bez wsparcia dla leków generycznych?

Uwaga: Chciałbym powiedzieć, że moje rozumienie teorii typów / bezpieczeństwa / wariancji / itd. Było bardziej kompletne; Nie jestem CS major.

Sytuacja

Masz klasę abstrakcyjną,Consumer, które chciałbyś rozszerzyć.Consumer deklaruje metodę abstrakcyjnąconsume(Argument $argument) który wymaga definicji. Nie powinno być problemu.

Problem

Twój wyspecjalizowanyConsumer, nazywaSpecializedConsumer nie ma logicznej współpracykażdy typArgument. Zamiast tego powinien zaakceptowaćSpecializedArgument (i ich podklasy). Nasz podpis metody zmienia się naconsume(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.
    }
}

Łamiemy sięZasada substytucji Liskowai powodujące problemy z bezpieczeństwem typów. Rufa.

Pytanie

Ok, więc to nie zadziała. Jednak biorąc pod uwagę tę sytuację, jakie istnieją wzorce lub strategie, aby pokonać problem bezpieczeństwa typu i naruszenieLSP, ale nadal utrzymuj relację typuSpecializedConsumer doConsumer?

Przypuszczam, że jest całkowicie do przyjęcia, że ​​odpowiedź może zostać poddana destylacji do „yun dofeded, z powrotem do deski kreślarskiej„

Rozważania, szczegóły i errata

W porządku, natychmiastowe rozwiązanie wygląda jak „nie definiujconsume() metoda wConsumer„. Ok, to ma sens, ponieważ deklaracja metody jest tak dobra, jak podpis. Semantycznie, chociaż nie maconsume(), nawet z nieznaną listą parametrów, trochę boli mój mózg. Być może jest lepszy sposób.

Z tego, co czytam, niewiele języków obsługuje kowariancję typu parametru; PHP jest jednym z nich i jest tutaj językiem implementacyjnym. Dalsze komplikacje, widziałem kreatywne ”rozwiązania„angażującygeneryczne; inna funkcja nie jest obsługiwana w PHP.

Z WikiWariancja (informatyka) - Potrzeba kowariancji typów argumentów?:

Stwarza to problemy w niektórych sytuacjach, gdzie typy argumentów powinny być kowariantne w celu modelowania wymagań rzeczywistych. Załóżmy, że masz klasę reprezentującą osobę. Osoba może zobaczyć się z lekarzem, więc ta klasa może mieć wirtualną próżnięPerson::see(Doctor d). Załóżmy teraz, że chcesz utworzyć podklasęPerson klasa,Child. To jestChild jest osobą. Można wtedy chcieć stworzyć podklasęDoctor, Pediatrician. Jeśli dzieci odwiedzają tylko pediatrów, chcielibyśmy to wprowadzić w systemie typu. Jednak naiwna implementacja nie powiedzie się: ponieważ aChild jestPerson, Child::see(d) musi wziąć każdyDoctor, nie tylkoPediatrician.

Artykuł mówi dalej:

W tym przypadkuwzór gościa może być wykorzystany do egzekwowania tego związku. Innym sposobem rozwiązania problemów w C ++ jest użycieprogramowanie ogólne.

Jeszcze raz,generyczne może być twórczo wykorzystany do rozwiązania problemu. Badamwzór gościa, ponieważ zresztą i tak mam częściowo wdrożoną implementację, jednak większość implementacji opisanych w artykułach wykorzystuje przeciążanie metody, kolejną nieobsługiwaną funkcję w PHP.

<too-much-information>

Realizacja

Ze względu na niedawną dyskusję omówię szczegółowe szczegóły implementacji, których nie uwzględniłem (jak w, prawdopodobnie zawrę zbyt wiele).

Dla zwięzłości wykluczyłem ciała metod dla tych, które są (powinno być) całkowicie jasne w ich celu. Jawypróbowany aby zachować to zwięźle, ale mam tendencję do bycia słownym. Nie chciałem zrzucać ściany kodu, więc wyjaśnienia następują / poprzedzają bloki kodu. Jeśli masz uprawnienia do edycji i chcesz to wyczyścić, zrób to. Ponadto bloki kodu nie są kopią makaronu z projektu. Jeśli coś nie ma sensu, może nie; krzyczeć na mnie o wyjaśnienie.

W odniesieniu do pierwotnego pytania, poniżejRule klasa jestConsumer iAdapter klasa jestArgument.

Klasy związane z drzewem są następujące:

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

Więc każdyInnerNode zawieraRule iNode obiekty i każdyOuterNode tylkoRule przedmioty.Node::evaluate() ocenia każdyRule (Node::evaluateEachRule()) do wartości logicznejtrue. Jeśli każdyRule przechodzi,Node minął i jestCommand jest dodawany doWrapperi zstąpi do dzieci do oceny (OuterNode::evaluateEachNode()) lub po prostu wróćtrue, dlaInnerNode iOuterNode obiekty odpowiednio.

Co się tyczyWrapper;Wrapper obiekty proxyRequest obiekt i ma kolekcjęAdapter przedmioty. TheRequest obiekt jest reprezentacją żądania HTTP. TheAdapter obiekt to specjalistyczny interfejs (i utrzymuje określony stan) do konkretnego zastosowania ze specyficznymRule przedmioty. (tu pojawiają się problemy z LSP)

TheCommand obiekt to akcja (porządnie zapakowany callback, naprawdę), który jest dodawany doWrapper obiekt, gdy wszystko zostanie powiedziane i zrobione, tablicaCommand obiekty zostaną wystrzelone w sekwencji, mijającRequest (między innymi) w.

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

Więc dany użytkownik-ziemiaRule akceptuje oczekiwaną ziemię użytkownikaAdapter. JeśliAdapter potrzebuje informacji o żądaniu, jest przekierowywaneWrapper, w celu zachowania integralności oryginałuRequest.

JakoWrapper agregatyAdapter obiekty, przekaże istniejące instancje do kolejnychRule obiekty, tak aby stanAdapter jest zachowany od jednegoRule do następnego. Pewnego razucały drzewo minęło,Wrapper::commit() jest nazywany, a każdy z zagregowanychAdapter obiekty zastosują swój stan w zależności od oryginałuRequest.

Pozostaje nam cały szeregCommand obiekty i zmodyfikowaneRequest.

O co chodzi, do diabła?

Cóż, nie chciałem odtworzyć prototypowej „tablicy routingu”, która jest powszechna w wielu frameworkach / aplikacjach PHP, więc zamiast tego poszedłem z „drzewkiem routingu”. Pozwalając na dowolne reguły, możesz szybko tworzyć i dołączaćAuthRule (na przykład) do aNodei już nie jest to, że cała gałąź jest dostępna bez przejściaAuthRule. W teorii (w mojej głowie) jest jak magiczny jednorożec, zapobiegający powielaniu kodu i wymuszający organizację strefy / modułu. W praktyce jestem zdezorientowany i przestraszony.

Dlaczego zostawiłem tę ścianę nonsensów?

Cóż, to jest implementacja, dla której muszę rozwiązać problem LSP. KażdyRule odpowiadaAdapteri to nie jest dobre. Chcę zachować związek między każdymRule, aby zapewnić bezpieczeństwo typu podczas konstruowania drzewa itp., jednak nie mogę zadeklarować metody klucza (evaluate()) w skrócieRule, jako zmiany podpisu dla podtypów.

Inna uwaga: pracuję nad uporządkowaniemAdapter schemat tworzenia / zarządzania; czy jest to odpowiedzialnośćRule stworzyć to itd.

</too-much-information>

questionAnswers(2)

yourAnswerToTheQuestion