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

时间:2012-07-15 17:07:06

标签: 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.
    }
}

我们正在打破Liskov substitution principle,并导致类型安全问题。船尾。


问题

好的,所以这不会起作用。但是,鉴于这种情况,存在哪些模式或策略可以克服类型安全问题,违反LSP,但仍然保持SpecializedConsumerConsumer的类型关系?

我认为完全可以接受的答案可以提炼为" ya dun goofed,回到绘图板"。


注意事项,细节和&勘误表

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

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

  • 来自Wiki' Variance (computer science) - Need for covariant argument types?

      

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

    文章接着说:

      

    在这种情况下,visitor pattern可用于强制执行此关系。在C ++中解决问题的另一种方法是使用generic programming

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


<too-much-information>

实施

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

  

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

关于原始问题,此后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
    }
}

因此,每个InnerNode包含RuleNode个对象,每个OuterNode仅包含Rule个对象。 Node::evaluate()会将每个Rule Node::evaluateEachRule() )评估为布尔值true。如果每个Rule通过,则Node已过,并且Command已添加到Wrapper,并将下降到子项以进行评估( {分别为OuterNode::evaluateEachNode()true个对象{1}} )或简单地返回InnerNode

至于OuterNode; Wrapper对象代理Wrapper对象,并具有Request个对象的集合。 Adapter对象是HTTP请求的表示。 Request对象是一个专用接口(并维护特定状态),以便与特定Adapter对象一起使用。 (这是LSP问题的来源

Rule对象是一个动作(一个整齐打包的回调,实际上是),它被添加到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); } 接受预期的用户土地Rule。如果Adapter需要有关请求的信息,则会通过Adapter进行路由,以保持原始Wrapper的完整性。

Request聚合Wrapper个对象时,它会将现有实例传递给后续Adapter个对象,以便保留Rule的状态{{1} }} 到下一个。一旦整个树已经过去,就会调用Adapter,并且每个聚合的Rule对象都会根据需要对原始Wrapper::commit()应用它的状态}。

然后我们留下一组Adapter个对象和一个经过修改的Request


重点是什么?

嗯,我不想重新创建原型&#34;路由表&#34;在许多PHP框架/应用程序中很常见,所以我选择了#34;路由树&#34;。通过允许任意规则,您可以快速创建Command例如)并将其附加到Request,并且不再传递{{1 }}。从理论上讲(在我脑海中),它就像一个神奇的独角兽,阻止了代码重复,并强制执行区域/模块组织。在实践中,我感到困惑和害怕。

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

嗯,这是我需要修复LSP问题的实现。每个AuthRule对应一个Node,而且不是很好。我想保留每个AuthRule之间的关系,以确保构建树时的类型安全性等,但是我不能声明密钥方法( Rule )在抽象Adapter中,因为子类型的签名发生了变化。

另一方面,我正在努力整理Rule创建/管理计划;是evaluate()是否有责任创建它等等。

Rule

2 个答案:

答案 0 :(得分:11)

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

真正的问题

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

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

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

策略

当我第一次阅读这个问题时,Strategy Pattern是我第一次想到的。基本上,它将实现细节与执行细节分开。它允许存在许多不同的“策略”,并且调用者将确定针对特定问题加载哪个“策略”。

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

命令

Command Pattern也会像战略一样解耦实施。主要区别在于,在策略中,调用者是选择消费者的人。在Command中,它是其他人(也许是工厂或调度员)......

每个“专业消费者”只会针对特定类型的问题实施逻辑。然后其他人会做出适当的选择。

责任链

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

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

访客模式

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

其他模式

最后,它实际上取决于您尝试解决的具体问题。如果您正在尝试处理HTTP请求,其中每个“消费者”处理不同的请求类型(XML与HTML与JSON等),最佳选择可能与您尝试处理查找几何区域时的情况非常不同一个多边形。当然,你可以使用相同的模式,但它们实际上不是同一个问题。

话虽如此,问题 也可以用Mediator Pattern来解决(在多个“消费者”需要机会处理数据的情况下),State Pattern (在“消费者”将取决于过去消费数据的情况下)或甚至是Adapter Pattern(在您在专业消费者中抽象不同子系统的情况下)......

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

答案 1 :(得分:4)

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

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