专业化中的参数类型协方差

Dan*_*ugg 26 php types covariance

TL;博士

在不支持泛型的语言(PHP)中,有哪些策略可以克服特殊化的参数类型不变性?

注意:我希望我可以说我对类型理论/安全/方差等的理解更完整; 我不是CS专业.


情况

你有一个抽象的类,Consumer你想扩展它.Consumer声明一个consume(Argument $argument)需要定义的抽象方法.应该不是问题.


问题

你的专业Consumer,被称为SpecializedConsumer没有逻辑与业务工作的每一个类型的Argument.相反,它应该接受一个SpecializedArgument(及其子类).我们的方法签名更改为consume(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.
    }
}
Run Code Online (Sandbox Code Playgroud)

我们打破Liskov替代原则,并导致类型安全问题.船尾.


好的,所以这不会起作用.然而,鉴于这种情况,有什么模式或策略存在克服类型的安全问题,以及违反LSP,但仍保持的类型的关系SpecializedConsumerConsumer

我想完全可以接受的是,答案可以被提炼为" ya dun goofed,回到绘图板 ".


考虑因素,细节和勘误表

  • 好吧,一个直接的解决方案表现为" 不要定义consume()方法Consumer ".好吧,这是有道理的,因为方法声明只有签名一样好.在语义上虽然没有consume(),即使有一个未知的参数列表,也会伤害我的大脑.也许有更好的方法.

  • 从我正在阅读的内容来看,很少有语言支持参数类型协方差; PHP就是其中之一,并且是这里的实现语言.更复杂的是,我看到了涉及泛型的创意" 解决方案 " ; PHP不支持的另一个功能.

  • 从维基的方差(计算机科学) - 需要协变论证类型?:

    这会在某些情况下产生问题,其中参数类型应该与模拟现实生活中的需求相协调.假设你有一个代表一个人的班级.一个人可以看医生,所以这个班可能有一个方法虚拟无效Person::see(Doctor d).现在假设你想要创建类的子Person类,Child.也就是说,a Child是一个人.然后人们可能想要创建一个子类Doctor,Pediatrician.如果孩子只访问儿科医生,我们希望在类型系统中强制执行.然而,一个天真的实现失败:因为a Child是a Person,Child::see(d)必须采取任何Doctor,而不仅仅是a Pediatrician.

    文章接着说:

    在这种情况下,访客模式可用于强制执行此关系.在C++中解决问题的另一种方法是使用泛型编程.

    同样,可以创造性地使用泛型来解决问题.我正在探索访问者模式,因为我无论如何都有一个半生不熟的实现,但是文章中描述的大多数实现都利用了方法重载,而PHP中还有另一个不受支持的功能.


<too-much-information>

履行

由于最近的讨论,我将扩展我忽略的具体实现细节(因为,我可能会包括太多).

为了简洁起见,我已经排除了那些(应该)非常清楚其目的的方法体.我试图保持这个简短,但我倾向于罗嗦.我不想转储一堆代码,因此解释在代码块之后/之前.如果你有编辑权限,并希望清理它,请执行.此外,代码块不是项目中的copy-pasta.如果某些事情没有意义,那可能不会; 对我大喊大叫澄清一下.

关于原始问题,此后Rule类是ConsumerAdapter类是Argument.

与树相关的类包括如下:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,每个InnerNode包含RuleNode对象,每个OuterNode唯一Rule对象.Node::evaluate()将each Rule(Node::evaluateEachRule())计算为布尔值true.如果每个都Rule通过,Node则已经过了,它将Command被添加到Wrapper,并将下降到子项以进行evaluate(OuterNode::evaluateEachNode()),或者只是分别返回truefor InnerNodeOuterNode对象.

至于Wrapper; 该Wrapper对象代理一个Request对象,并具有集合Adapter对象.该Request对象是HTTP请求的表示.该Adapter对象是一个专用接口(并保持特定状态),用于特定Rule对象的特定用途.(这是LSP问题的来源)

Command物体是一个动作(一个包装精美的回调,真的被添加到)Wrapper对象,一旦一切都说过和做过,数组Command对象将按顺序被解雇,传递Request(除其他事项外)英寸

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);
}
Run Code Online (Sandbox Code Playgroud)

因此,给定的用户土地Rule接受预期的用户土地Adapter.如果Adapter需要有关请求的信息,则将其路由Wrapper,以保持原始的完整性Request.

作为Wrapper聚合Adapter对象,它将现有实例传递给后续Rule对象,以便将a的状态Adapter从一个保存Rule到下一个.一旦整个树通过,Wrapper::commit()就会被调用,并且每个聚合Adapter对象将根据需要对原始树应用它的状态Request.

然后我们留下一个Command对象数组和一个修改过的对象Request.


到底是什么意思?

好吧,我不想重新创建许多PHP框架/应用程序中常见的原型"路由表",所以我选择了"路由树".通过允许任意规则,您可以快速创建并附加AuthRule(例如)到a Node,并且不再可以访问整个分支而不传递AuthRule.理论上(在我的脑海中)它就像一个神奇的独角兽,防止代码重复,并强制执行区域/模块组织.在实践中,我感到困惑和害怕.

为什么我离开了这堵废话?

好吧,这是我需要修复LSP问题的实现.每个都Rule对应一个Adapter,这并不好.我想保留每个之间的关系Rule,以确保构造树时的类型安全性等,但是我不能evaluate()在摘要中声明关键方法()Rule,因为子类型的签名会发生变化.

另一方面,我正在努力整理Adapter创建/管理方案; 是否有责任Rule创造它等

</too-much-information>

irc*_*ell 11

要正确回答这个问题,我们必须退后一步,看看你试图以更一般的方式解决的问题(你的问题已经非常普遍).

真正的问题

真正的问题是你正在尝试使用继承来解决业务逻辑问题.由于LSP违规而且更重要的是将业务逻辑紧密耦合到应用程序的结构,因此永远不会起作用.

因此,继承作为解决此问题的方法(对于上述问题以及您在问题中说明的原因).幸运的是,我们可以使用许多组合模式.

现在,考虑到你的问题是如何通用的,很难找到解决问题的可靠方法.那么让我们来看几个模式,看看他们如何解决这个问题.

战略

策略模式是,来到我的脑海里,当我第一次读到这个问题第一.基本上,它将实现细节与执行细节分开.它允许存在许多不同的"策略",并且调用者将确定针对特定问题加载哪个"策略".

这里的缺点是呼叫者必须知道策略才能选择正确的策略.但它也允许更清楚地区分不同的策略,所以这是一个不错的选择......

命令

命令模式也将脱钩就像策略将对执行.主要区别在于,在策略中,调用者是选择消费者的人.在Command中,它是其他人(也许是工厂或调度员)......

每个"专业消费者"都只实现特定类型问题的逻辑.然后其他人会做出适当的选择.

责任链

可能适用的下一个模式是责任链模式.这与上面讨论的策略模式类似,不同之处在于,不是消费者决定调用哪个策略,而是按顺序调用每个策略,直到处理请求为止.因此,在您的示例中,您将采用更通用的参数,但检查它是否是特定的参数.如果是,请处理请求.否则,让下一个尝试一下......

桥模式可适当这里.这在某种意义上类似于策略模式,但它的不同之处在于桥接实现将在构建时而不是在运行时选择策略.因此,您将为每个实现构建一个不同的"使用者",其中的详细信息在内部作为依赖项组成.

访客模式

您在问题中提到了访客模式,所以我想我会在这里提到它.我不确定它在这种情况下是否合适,因为访问者实际上类似于旨在遍历结构的策略模式.如果您没有要遍历的数据结构,那么访问者模式将被提炼为与策略模式非常相似.我公平地说,因为控制的方向不同,但结束关系几乎是一样的.

其他模式

最后,它实际上取决于您试图解决的具体问题.如果你想处理HTTP请求,其中每个"消费者"处理不同的请求类型(XML VS HTML VS JSON等),最好的选择可能会比如果你试图处理发现的几何面积很大的不同一个多边形.当然,你可以对两者使用相同的模式,但它们实际上不是同一个问题.

随着中说,这个问题可能也可以用解决调解模式(在该情况下多个"消费者"需要一个机会来处理数据),一国模式(在该情况下"消费者"将依赖于过去的消费数据)甚至是适配器模式(在你在专业消费者中抽象不同的子系统的情况下)......

简而言之,这是一个难以回答的问题,因为有太多的解决方案,很难说哪个是正确的......


dev*_*ler 5

我唯一知道的是 DIY 策略:接受简单Argument的函数定义并立即检查它是否足够专业:

class SpecializedConsumer extends Consumer {
    public function consume(Argument $argument) {
        if(!($argument instanceof SpecializedArgument)) {
            throw new InvalidArgumentException('Argument was not specialized.');
        }
        // move on
    }
}
Run Code Online (Sandbox Code Playgroud)