首页 文章

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

提问于
浏览
25

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,回到绘图板” .


注意事项,细节和勘误表

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

  • 从我_492809看到的涉及generics的创意“解决方案”; PHP中不支持的另一个功能 .

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

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

文章接着说:

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

同样,generics可以创造性地用于解决问题 . 我正在探索visitor pattern,因为无论如何我都有一个半生不熟的实现,但是文章中描述的大多数实现都利用了方法重载,这是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
    }
}

所以每个 InnerNode 包含 RuleNode 个对象,每个 OuterNode 只包含 Rule 个对象 . Node::evaluate() 将每个 RuleNode::evaluateEachRule() )计算为布尔值 true . 如果每个 Rule 都通过,则 Node 已经过去,它的 Command 被添加到 Wrapper ,并将下降到子项以进行评估( OuterNode::evaluateEachNode() ),或者只返回 true ,分别用于 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 . 一旦 an entire 树通过,就会调用 Wrapper::commit() ,并且每个聚合的 Adapter 对象将根据需要对原始 Request 应用它的状态 .

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


What the hell is the point?

好吧,我不想在许多PHP框架/应用程序中重新创建原型"routing table" common,所以我选择了"routing tree" . 通过允许任意规则,您可以快速创建 AuthRule (例如)到 Node ,并且在不传递 AuthRule 的情况下不再可以访问整个分支 . 在理论上(在我的脑海中),它让我感到困惑和害怕 .

Why I left this wall of nonsense?

好吧,这是我需要修复LSP问题的实现 . 每个 Rule 对应一个 Adapter ,这并不好 . 我想保持每个 Rule 之间的关系,以确保类型安全构建树时,等等,但是我不能抽象地 Rule 声明的主要手段( evaluate() ),作为亚型的特征变化 .

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

</too-much-information>

2 回答

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

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

    真正的问题

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

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

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

    战略

    当我第一次阅读这个问题时,Strategy Pattern是第一个出现在我脑海中的东西 . 基本上,它将实现细节与执行细节分开 . 它允许存在许多不同的"strategies",并且调用者将确定为特定问题加载哪个 .

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

    命令

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

    每个"Specialized Consumer"只会为特定类型的问题实现逻辑 . 然后其他人会做出适当的选择 .

    责任链

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

    这里也可能适合Bridge Pattern . 这在某种意义上类似于策略模式,但它的不同之处在于桥接实现会在构建时而不是在运行时选择策略 . 那么你将为每个实现构建一个不同的"consumer",其中的细节作为依赖项组成 .

    访客模式

    你在问题中提到了Visitor Pattern,所以我在这里提到它 . 我在这个上下文中是合适的,因为访问者实际上类似于具有要遍历的数据结构的策略模式,那么访问者模式将被提炼为看起来与策略模式非常相似 . 我公平地说,因为控制的方向不同,但最终的关系几乎是一样的 .

    其他模式

    最后,这真的取决于具体问题,你're trying to solve. If you'重新尝试处理HTTP请求,其中每个"Consumer"处理不同的请求类型(XML VS HTML VS JSON等),最好的选择可能会比,如果你很不同的”重新尝试处理找到多边形的几何区域 . 当然,你可以对两者使用相同的模式,但它们实际上不是同一个问题 .

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

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

相关问题