【问题标题】:Parameter type covariance in specializations专业化中的参数类型协方差
【发布时间】:2012-07-14 16:28:28
【问题描述】:

tl;博士

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

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


情况

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


问题

您的专业Consumer,称为SpecializedConsumerArgument每个 类型没有逻辑业务。相反,它应该接受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 的类型关系?

我认为可以将答案提炼为“你别傻了,回到绘图板”是完全可以接受的。


注意事项、详细信息和勘误表

  • 好的,立即解决方案显示为“不要在 Consumer 中定义 consume() 方法”。好的,这是有道理的,因为方法声明与签名一样好。尽管没有consume() 语义上,即使有一个未知的参数列表,也会让我的大脑有点受伤。也许有更好的方法。

  • 根据我的阅读,很少有语言支持参数类型协方差; PHP 就是其中之一,并且是这里的实现语言。更复杂的事情是,我看到了涉及generics 的创造性“解决方案”; PHP 不支持的另一个功能。

  • 来自维基的Variance (computer science) - Need for covariant argument types?

    这在某些情况下会产生问题,其中参数类型应该是协变的以模拟现实生活中的需求。假设你有一个代表一个人的类。一个人可以看医生,所以这个类可能有一个方法 virtual void 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()),或者简单地返回@ 987654375@,分别代表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);
}

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

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

然后我们得到一个 Command 对象数组和一个修改后的 Request


这到底是什么意思?

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

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

嗯,这是我需要解决 LSP 问题的实现。每个Rule 对应一个Adapter,这不好。我想保留每个Rule之间的关系,以确保构造树时的类型安全等,但是我不能在抽象@中声明关键方法(evaluate()) 987654412@,随着子类型的签名发生变化。

另一方面,我正在整理Adapter 创建/管理方案;是否由Rule 负责创建等等。

</too-much-information>

【问题讨论】:

  • 我目前没有时间深入研究,但恕我直言,这是我更喜欢composition over inheritance 的一个重要原因。您的问题出现是因为您首先尝试扩展摘要。我最初的建议是使用接口而不是抽象类,在方法签名中对接口进行类型提示,并根据需要通过构造函数组合功能。
  • @Dan 我发现自己在问你多年前提出的这个问题。你还记得你走的是哪条路吗?从那以后你的想法有改变吗?

标签: php types covariance


【解决方案1】:

要正确回答这个问题,我们必须真正退后一步,以更笼统的方式看待您要解决的问题(而且您的问题已经很笼统了)。

真正的问题

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

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

现在,考虑到您的问题的普遍性,很难确定一个可靠的解决方案来解决您的问题。因此,让我们回顾一些模式,看看它们如何解决这个问题。

策略

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

这里的缺点是调用者必须了解策略才能选择正确的策略。但它也允许更清晰地区分不同的策略,所以这是一个不错的选择......

命令

Command Pattern 也可以像 Strategy 那样解耦实现。主要区别在于,在 Strategy 中,调用者是选择消费者的那个。在 Command 中,是其他人(可能是工厂或调度员)...

每个“专业消费者”将仅实现特定类型问题的逻辑。那么其他人会做出适当的选择。

责任链

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

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

访客模式

您在问题中提到了Visitor Pattern,所以我想在这里提一下。我不确定它在这种情况下是否合适,因为访问者与旨在遍历结构的策略模式非常相似。如果您没有要遍历的数据结构,那么访客模式将被提炼成看起来与策略模式非常相似。我说的比较公道,因为控制的方向不同,但最终的关系却大同小异。

其他模式

最后,这实际上取决于您要解决的具体问题。如果您尝试处理 HTTP 请求,其中每个“消费者”处理不同的请求类型(XML、HTML 和 JSON 等),那么最佳选择可能与您尝试处理查找一个多边形。当然,您可以对两者使用相同的模式,但它们并不是真正的同一个问题。

话虽如此,问题也可以通过Mediator Pattern(在多个“消费者”需要有机会处理数据的情况下)、State Pattern(在这种情况下其中“消费者”将取决于过去消费的数据)甚至Adapter Pattern(如果您在专门的消费者中抽象出不同的子系统)...

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

    • 感谢@dev-null-dweller。当然,我已经考虑过(忽略修改);这可能是我必须采取的方向。我已经采用了“...尽管 PHP 是动态类型的,并且不需要在函数声明中输入形式参数,如果一个合理的 IDE 词法分析器/解析器可以跟随我,那么类型安全已经有点实现。”这听起来可能很傻,但它使我免于引入含糊不清。条件检查作为伪向下转换,这可能再次起作用。 (我讨厌在谈论 PHP 时必须在所有内容前面加上“伪”
    • 这违反了 LSP 以及 Consumer::consume 方法没有抛出 InvalidArgumentException。你只是绕过 php 的检查让你这样做。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-23
    • 1970-01-01
    • 2014-09-12
    相关资源
    最近更新 更多