专业化中的参数类型协方差 [英] Parameter type covariance in specializations

查看:70
本文介绍了专业化中的参数类型协方差的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

What strategies exist to overcome parameter type invariance for specializations, in a language (PHP) without support for generics?

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

您有一个要扩展的抽象类Consumer. Consumer声明需要定义的抽象方法consume(Argument $argument).应该没问题.

You've got an abstract class, Consumer, that you'd like to extend. Consumer declares an abstract method consume(Argument $argument) which needs a definition. Shouldn't be a problem.

您称为SpecializedConsumer的专用Consumer类型的Argument都没有逻辑关系.相反,它应该接受SpecializedArgument(及其子类).我们的方法签名更改为consume(SpecializedArgument $argument).

Your specialized Consumer, called SpecializedConsumer has no logical business working with every type of Argument. Instead, it should accept a SpecializedArgument (and subclasses thereof). Our method signature changes to 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.
    }
}

我们违反了 Liskov替换原则,并导致类型安全问题.大便.

We're breaking Liskov substitution principle, and causing type safety problems. Poop.

好的,所以这行不通.但是,在这种情况下,存在哪些模式或策略可以克服类型安全问题,并且违反了 LSP ,但仍然保持SpecializedConsumerConsumer的类型关系?

Ok, so this isn't going to work. However, given this situation, what patterns or strategies exist to overcome the type safety problem, and the violation of LSP, yet still maintain the type relationship of SpecializedConsumer to Consumer?

我认为答案可以精炼为"愚蠢,回到绘图板"是完全可以接受的.

I suppose it's perfectly acceptable that an answer can be distilled down to "ya dun goofed, back to the drawing board".

  • 好的,立即解决方案表示为"不要在Consumer 中定义consume()方法".好的,这很有意义,因为方法声明仅与签名一样好.从语义上来说,即使缺少consume(),即使参数列表未知,也会使我的大脑有些受伤.也许有更好的方法.

  • Alright, an immediate solution presents itself as "don't define the consume() method in Consumer". Ok, that makes sense, because method declaration is only as good as the signature. Semantically though the absence of consume(), even with a unknown parameter list, hurts my brain a bit. Perhaps there is a better way.

根据我的阅读,很少有语言支持参数类型协方差; PHP是其中之一,并且是此处的实现语言.使事情变得更加复杂的是,我看到了涉及"泛型的创造性的"解决方案" ; PHP不支持的另一个功能.

From what I'm reading, few languages support parameter type covariance; PHP is one of them, and is the implementation language here. Further complicating things, I've seen creative "solutions" involving generics; another feature not supported in PHP.

来自Wiki的方差(计算机科学)-需要协变参数类型?:

这在某些情况下会产生问题,在这种情况下,参数类型应协变量以模拟现实生活中的需求.假设您有一个代表人的班级.一个人可以看医生,因此此类可能有一个方法virtual void Person::see(Doctor d).现在假设您要创建Person类的子类Child.也就是说,Child是一个人.然后,您可能希望创建DoctorPediatrician的子类.如果孩子只拜访儿科医生,我们希望在类型系统中强制执行.但是,幼稚的实现会失败:因为ChildPerson,所以Child::see(d)必须采用任何Doctor,而不仅仅是Pediatrician.

This creates problems in some situations, where argument types should be covariant to model real-life requirements. Suppose you have a class representing a person. A person can see the doctor, so this class might have a method virtual void Person::see(Doctor d). Now suppose you want to make a subclass of the Person class, Child. That is, a Child is a Person. One might then like to make a subclass of Doctor, Pediatrician. If children only visit pediatricians, we would like to enforce that in the type system. However, a naive implementation fails: because a Child is a Person, Child::see(d) must take any Doctor, not just a Pediatrician.

文章继续说:

在这种情况下,可以使用访问者模式来加强这种关系.解决问题的另一种方法是在C ++中,使用泛型编程.

In this case, the visitor pattern could be used to enforce this relationship. Another way to solve the problems, in C++, is using generic programming.

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

Again, generics can be used creatively to solve the problem. I'm exploring the visitor pattern, as I have a half-baked implementation of it anyway, however most implementations as described in articles leverage method overloading, yet another unsupported feature in PHP.

<too-much-information>

由于最近的讨论,我将详细介绍我忽略包括的具体实现细节(例如,,我可能会包含太多).

Due to recent discussion, I'll expand on the specific implementation details I've neglected to include (as in, I'll probably include way too much).

为简洁起见,我已经排除了方法主体,因为那些方法主体(应该是)的目的非常明确.为了保持简短,我已经试过了,但是我倾向于罗word.我不想丢掉一堵代码墙,因此在代码块之前/之后进行解释.如果您具有编辑权限,并且想要清理此权限,请这样做.另外,代码块不是从项目复制粘贴的.如果某些事情没有意义,那就可能没有意义.大喊大叫以求澄清.

For brevity, I've excluded method bodies for those which are (should be) abundantly clear in their purpose. I've tried to keep this brief, but I tend to get wordy. I didn't want to dump a wall of code, so explanations follow/precede code blocks. If you have edit privileges, and want to clean this up, please do. Also, code blocks aren't copy-pasta from a project. If something doesn't make sense, it might not; yell at me for clarification.

关于原始问题,此后Rule类是Consumer,而Adapter类是Argument.

With respect to the original question, hereafter the Rule class is the Consumer and the Adapter class is the Argument.

与树相关的类包括以下内容:

The tree-related classes are comprised as follows:

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

因此,每个InnerNode都包含RuleNode对象,而每个OuterNode仅包含Rule对象. Node::evaluate()将每个Rule( Node::evaluateEachRule() )评估为布尔值true.如果每个Rule通过,则Node已经通过,并将它的Command添加到Wrapper,并将下降到子级进行评估( OuterNode::evaluateEachNode() ),或者简单地返回true,分别用于InnerNodeOuterNode对象.

So each InnerNode contains Rule and Node objects, and each OuterNode only Rule objects. Node::evaluate() evaluates each Rule (Node::evaluateEachRule()) to a boolean true. If each Rule passes, the Node has passed and it's Command is added to the Wrapper, and will descend to children for evaluation (OuterNode::evaluateEachNode()), or simply return true, for InnerNode and OuterNode objects respectively.

至于WrapperWrapper对象代理Request对象,并具有Adapter对象的集合. Request对象是HTTP请求的表示. Adapter对象是专用于特定Rule对象的专用接口(并保持特定状态). (这就是LSP问题出现的地方)

As for Wrapper; the Wrapper object proxies a Request object, and has a collection of Adapter objects. The Request object is a representation of the HTTP request. The Adapter object is a specialized interface (and maintains specific state) for specific use with specific Rule objects. (this is where the LSP problems come in)

Command对象是一个动作(一个整齐地包装好的回调,实际上),它被添加到Wrapper对象中,一旦全部说完,就组成了Command对象的数组会被依次触发,并传递Request(其中的 ).

The Command object is an action (a neatly packaged callback, really) which is added to the Wrapper object, once all is said and done, the array of Command objects will be fired in sequence, passing the Request (among other things) in.

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

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

So a given user-land Rule accepts the expected user-land Adapter. If the Adapter needs information about the request, it's routed through Wrapper, in order to preserve the integrity of the original Request.

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

As the Wrapper aggregates Adapter objects, it will pass existing instances to subsequent Rule objects, so that the state of an Adapter is preserved from one Rule to the next. Once an entire tree has passed, Wrapper::commit() is called, and each of the aggregated Adapter objects will apply it's state as necessary against the original Request.

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

We are then left with an array of Command objects, and a modified Request.

到底是什么?

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

Well, I didn't want to recreate the prototypical "routing table" common in many PHP frameworks/applications, so instead I went with a "routing tree". By allowing arbitrary rules, you can quickly create and append an AuthRule (for example) to a Node, and no longer is that whole branch accessible without passing the AuthRule. In theory (in my head) it's like a magical unicorn, preventing code duplication, and enforcing zone/module organization. In practice, I'm confused and scared.

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

嗯,这是我需要解决LSP问题的实现.每个Rule都对应一个Adapter,但这并不好.我想保留每个Rule之间的关系,以确保构造树等时的类型安全,但是我无法在抽象<中声明关键方法( evaluate() )Rule,因为子类型的签名会更改.

Well, this is the implementation for which I need to fix the LSP problem. Each Rule corresponds to an Adapter, and that ain't good. I want to preserve the relationship between each Rule, as to ensure type safety when constructing the tree, etc., however I can't declare the key method (evaluate()) in the abstract Rule, as the signature changes for subtypes.

另一方面,我正在努力整理Adapter创建/管理方案; Rule负责创建它,等等.

On another note, I'm working on sorting out the Adapter creation/management scheme; whether it is the responsibility of the Rule to create it, etc.

</too-much-information>

推荐答案

要正确回答此问题,我们必须退后一步,以更一般的方式查看您要解决的问题(以及您的问题已经很一般了.

To properly answer this question, we must really take a step back and look at the problem you're trying to solve in a more general manner (and your question was already pretty general).

真正的问题是您试图使用继承来解决业务逻辑问题.由于违反了LSP,而且更重要的是,将业务逻辑紧密耦合到应用程序的结构,所以这永远行不通.

The real problem is that you're trying to use inheritance to solve a problem of business logic. That's never going to work because of LSP violations and -more importantly- tight coupling your business logic to the application's structure.

因此,将继承作为解决此问题的方法(针对上述情况以及您在问题中所述的原因).幸运的是,我们可以使用许多合成模式.

So inheritance is out as a method to solve this problem (for the above, and the reasons you stated in the question). Fortunately, there are a number of compositional patterns that we can use.

现在,考虑到您的问题的通用性,要确定一个可靠的解决方案将非常困难.因此,让我们看一下几种模式,看看它们如何解决这个问题.

Now, considering how generic your question is, it's going to be very hard to identify a solid solution to your problem. So let's go over a few patterns and see how they can solve this problem.

策略模式是我第一次阅读该问题时想到的第一个.基本上,它将实现细节与执行细节分开.它允许存在许多不同的策略",并且调用方将确定为特定问题加载哪种策略.

The Strategy Pattern is the first that came to my mind when I first read the question. Basically, it separates the implementation details from the execution details. It allows for a number of different "strategies" to exist, and the caller would determine which to load for the particular problem.

这里的缺点是呼叫者必须了解策略才能选择正确的策略.但这也使不同策略之间可以有一个更清晰的区分,因此这是一个不错的选择...

The downside here is that the caller must know about the strategies in order to pick the correct one. But it also allows for a cleaner distinction between the different strategies, so it's a decent choice...

命令模式也将像Strategy那样使实现分离.主要区别在于,在Strategy中,呼叫者是选择消费者的人.在Command中,是其他人(也许是工厂或调度员)...

The Command Pattern would also decouple the implementation just like Strategy would. The main difference is that in Strategy, the caller is the one that chooses the consumer. In Command, it's someone else (a factory or dispatcher perhaps)...

每个特殊消费者"将仅实现针对特定类型问题的逻辑.然后其他人会做出适当的选择.

Each "Specialized Consumer" would implement only the logic for a specific type of problem. Then someone else would make the appropriate choice.

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

The next pattern that may be applicable is the Chain of Responsibility Pattern. This is similar to the strategy pattern discussed above, except that instead of the consumer deciding which is called, each one of the strategies is called in sequence until one handles the request. So, in your example, you would take the more generic argument, but check if it's the specific one. If it is, handle the request. Otherwise, let the next one give it a try...

在这里也可以使用桥接模式.从某种意义上说,这与策略"模式相似,但是不同之处在于,桥梁实施将在构建时而不是在运行时选择策略.因此,您将为每个实现构建一个不同的消费者",并将内部组成的详细信息作为依赖项.

A Bridge Pattern may be appropriate here as well. This is in some sense similar to the Strategy pattern, but it's different in that a bridge implementation would pick the strategy at construction time, instead of at run time. So then you would build a different "consumer" for each implementation, with the details composed inside as dependencies.

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

You mentioned the Visitor Pattern in your question, so I'd figure I'd mention it here. I'm not really sure it's appropriate in this context, because a visitor is really similar to a strategy pattern that's designed to traverse a structure. If you don't have a data structure to traverse, then the visitor pattern will be distilled to look fairly similar to a strategy pattern. I say fairly, because the direction of control is different, but the end relationship is pretty much the same.

最后,这实际上取决于您要解决的具体问题.如果您尝试处理HTTP请求,而每个消费者"都处理不同的请求类型(XML,HTML,JSON等),则最佳选择可能会与尝试查找对象的几何区域的方法大不相同.一个多边形.当然,您可以可以对两者使用相同的模式,但是它们并不是真正的相同问题.

In the end, it really depends on the concrete problem that you're trying to solve. If you're trying to handle HTTP requests, where each "Consumer" handles a different request type (XML vs HTML vs JSON etc), the best choice will likely be very different than if you're trying to handle finding the geometric area of a polygon. Sure, you could use the same pattern for both, but they are not really the same problem.

话虽如此,问题 也可以通过 Mediator Pattern 解决(在这种情况下,如果有多个消费者"需要处理数据的机会),则状态模式(如果消费者"取决于过去消耗的数据)甚至是适配器模式(如果您要在专用使用者中抽象一个不同的子系统) ...

With that said, the problem could also be solved with a Mediator Pattern (in the case where multiple "Consumers" need a chance to process data), a State Pattern (in the case where the "Consumer" will depend on past consumed data) or even an Adapter Pattern (in the case where you're abstracting a different sub-system in the specialized consumer)...

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

In short, it's a difficult problem to answer, because there are so many solutions that it's hard to say which is correct...

这篇关于专业化中的参数类型协方差的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆