Covariância do tipo de parâmetro em especializações

tl; dr

Quais estratégias existem para superar a invariância do tipo de parâmetro para especializações, em uma linguagem (PHP) sem suporte para genéricos?

Nota: Eu gostaria de poder dizer que o meu entendimento da teoria dos tipos / segurança / variância / etc., Foi mais completo; Eu não sou CS major.

Situação

Você tem uma aula abstrataConsumer, que você gostaria de estender.Consumer declara um método abstratoconsume(Argument $argument) que precisa de uma definição. Não deve ser um problema.

Problema

Sua especializadaConsumer, chamadoSpecializedConsumer não tem negócios lógicos trabalhando comcada tipo deArgument. Em vez disso, deve aceitar umSpecializedArgument (e subclasses dos mesmos). Nossa assinatura de método muda paraconsume(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.
    }
}

Nós estamos quebrandoPrincípio da substituição de Liskove causando problemas de segurança de tipo. Cocô.

Questão

Ok, isso não vai funcionar. No entanto, dada esta situação, que padrões ou estratégias existem para superar o tipo de problema de segurança, e a violação deLSP, ainda manter o tipo de relação deSpecializedConsumer paraConsumer?

Eu suponho que é perfeitamente aceitável que uma resposta possa ser reduzida a "ya dun goofed, de volta para a prancheta de desenho".

Considerações, detalhes e errata

Tudo bem, uma solução imediata se apresenta como "não defina oconsume() método emConsumer". Ok, isso faz sentido, porque a declaração do método é tão boa quanto a assinatura. Semanticamente, a ausência deconsume(), mesmo com uma lista de parâmetros desconhecidos, dói um pouco meu cérebro. Talvez haja um caminho melhor.

Pelo que estou lendo, poucas linguagens suportam a covariância do tipo de parâmetro; PHP é um deles, e é a linguagem de implementação aqui. Mais complicando as coisas, eu vi criativo "soluções"envolvendogenéricos; outro recurso não suportado no PHP.

Do WikiVariação (ciência da computação) - Necessidade de tipos de argumentos covariantes?:

Isso cria problemas em algumas situações, em que os tipos de argumentos devem ser covariantes para modelar os requisitos da vida real. Suponha que você tenha uma classe representando uma pessoa. Uma pessoa pode ver o médico, então essa classe pode ter um método vazio virtualPerson::see(Doctor d). Agora, suponha que você queira criar uma subclasse doPerson classe,Child. Aquilo é umChild é uma pessoa. Pode-se então gostar de fazer uma subclasse deDoctor, Pediatrician. Se as crianças visitam apenas pediatras, gostaríamos de aplicar isso no sistema de tipos. No entanto, uma implementação ingênua falha: porqueChild é umPerson, Child::see(d) deve tomar qualquerDoctornão apenas umPediatrician.

O artigo continua dizendo:

Neste caso, opadrão de visitante poderia ser usado para impor esse relacionamento. Outra maneira de resolver os problemas, em C ++, é usandoprogramação genérica.

Novamente,genéricos pode ser usado criativamente para resolver o problema. Estou explorando opadrão de visitante, como eu tenho uma implementação semi-elaborada de qualquer maneira, no entanto, a maioria das implementações, conforme descrito nos artigos, alavanca a sobrecarga do método, mais um recurso não suportado no PHP.

<too-much-information>

Implementação

Devido à recente discussão, expandirei os detalhes específicos da implementação que negligenciei incluir (como em, eu provavelmente vou incluir demais).

Por questões de brevidade, eu excluí os corpos de métodos para aqueles que são (deveria estar) muito claro em seu propósito. Eu tenhotentou para manter isso breve, mas eu tendem a ser prolixo. Eu não queria despejar uma parede de código, então as explicações seguem / precedem os blocos de código. Se você tem privilégios de edição, e quer limpar isto, por favor faça. Além disso, blocos de código não são massas de cópia de um projeto. Se algo não faz sentido, talvez não; grite comigo por esclarecimento.

Com relação à questão original, doravante aRule classe é oConsumer e aAdapter classe é oArgument.

As classes relacionadas à árvore são compreendidas da seguinte forma:

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

Então cadaInnerNode contémRule eNode objetos, e cadaOuterNode sóRule objetos.Node::evaluate() avalia cadaRule (Node::evaluateEachRule()) para um booleanotrue. Se cadaRule passa, oNode passou e éCommand é adicionado aoWrapper, e descerá para as crianças para avaliação (OuterNode::evaluateEachNode()), ou simplesmente retornartrue, paraInnerNode eOuterNode objetos respectivamente.

Quanto aWrapper; aWrapper proxies objeto umRequest objeto, e tem uma coleção deAdapter objetos. oRequest objeto é uma representação da solicitação HTTP. oAdapter objeto é uma interface especializada (e mantém estado específico) para uso específico comRule objetos. (é aí que entram os problemas do LSP)

oCommand objeto é uma ação (um callback bem empacotado, realmente) que é adicionado aoWrapper objeto, uma vez que tudo é dito e feito, a matriz deCommand objetos serão disparados em seqüência, passando oRequest (entre outras coisas) em.

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

Então, uma determinada terra de usuárioRule aceita a terra do usuário esperadaAdapter. Se oAdapter precisa de informações sobre o pedido, é encaminhado atravésWrapper, a fim de preservar a integridade do originalRequest.

Enquanto oWrapper agregadosAdapter objetos, ele passará instâncias existentes para subsequentesRule objetos, para que o estado de umAdapter é preservado de umRule para o próximo. Uma vezum inteiro a árvore passouWrapper::commit() é chamado, e cada um dos agregadosAdapter objetos irá aplicar seu estado como necessário contra o originalRequest.

Nós somos então deixados com uma matriz deCommand objetos, e um modificadoRequest.

O que diabos é o ponto?

Bem, eu não queria recriar a "tabela de roteamento" prototípica comum em muitos frameworks / aplicações PHP, então eu fui com uma "árvore de roteamento". Ao permitir regras arbitrárias, você pode criar e anexar rapidamenteAuthRule (por exemplo) para umNode, e não é mais que toda a filial acessível sem passar oAuthRule. Em teoria (Na minha cabeça) é como um unicórnio mágico, impedindo a duplicação de código e impondo a organização de zona / módulo. Na prática, estou confuso e com medo.

Por que eu deixei essa parede de bobagens?

Bem, esta é a implementação para a qual eu preciso consertar o problema do LSP. CadaRule corresponde a umAdaptere isso não é bom. Eu quero preservar a relação entre cada umRule, como para garantir a segurança do tipo ao construir a árvore, etc., no entanto, não posso declarar o método-chave (evaluate()) no resumoRule, como a assinatura muda para subtipos.

Em outra nota, estou trabalhando em resolver oAdapter esquema de criação / gestão; se é da responsabilidade doRule para criá-lo, etc.

</too-much-information>

questionAnswers(2)

yourAnswerToTheQuestion