简述
在不支持泛型的编程语言(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.
}
}
我们正在违反里氏替换原则,并导致类型安全问题。糟糕。
问题
好的,这方法行不通。然而,在这种情况下,有哪些模式或策略可以克服类型安全问题和违反LSP,但仍然保持 SpecializedConsumer
与 Consumer
之间的类型关系?
我想一个可以接受的答案是:缩减到“你犯了错,回去重头开始”。
考虑事项、细节和勘误
好的,一个立即的解决方案呈现出来,就是"不在
Consumer
中定义consume()
方法"。好吧,这很有道理,因为方法声明只有签名那么好。语义上,即使有未知参数列表,缺少consume()
也会让我的大脑受到伤害。也许有更好的方法。从我所读的内容来看,很少有语言支持参数类型协变;PHP是其中之一,并且是这里的实现语言。更加复杂的是,我看到了创造性的"解决方案",涉及泛型;另一个在PHP中不支持的特性。
来自维基百科的协变(计算机科学)- 需要协变参数类型吗?:
这在某些情况下会产生问题,其中参数类型应该是协变的,以模拟现实需求。假设你有一个表示人的类。一个人可以看医生,因此这个类可能有一个方法virtual void
Person::see(Doctor d)
。现在假设你想要制作一个Person
类的子类Child
。也就是说,一个Child
是一个人。那么,我们可能想要制作一个Doctor
的子类Pediatrician
。如果儿童只看儿科医生,我们希望在类型系统中强制执行这一点。然而,一个天真的实现失败了:因为一个Child
是一个Person
,所以Child::see(d)
必须接受任何Doctor
,而不仅仅是Pediatrician
。文章继续说道:
同样,泛型可以创造性地用于解决问题。我正在探索访问者模式,因为我已经有了一个半成品的实现,但是大多数文章中描述的实现利用了方法重载,这也是PHP中不支持的另一个特性。
<太多信息>
实现
由于最近的讨论,我将扩展我未包含的具体实现细节(也就是说,我可能会包括过多的内容)。
为了简洁起见,我省略了那些(应该)在其目的上非常明确的方法体。我尝试保持简洁,但我往往会用很多话。我不想把一堵墙的代码倒出来,所以解释跟随/先于代码块。如果您有编辑权限,并想清理一下,请这样做。另外,代码块不是从项目中复制-粘贴的。如果某些东西不合理,可能就是这样;请指导我澄清。
关于原问题,此后 Rule
类是 Consumer
,而 Adapter
类是 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
包含Rule
和Node
对象,而每个OuterNode
只有Rule
对象。 Node::evaluate()
评估每个Rule
(Node::evaluateEachRule()
)为布尔值true
。如果每个Rule
都通过了,Node
已经通过,并将其Command
添加到Wrapper
中,并将下降到子级进行评估(OuterNode::evaluateEachNode()
),或者分别对于InnerNode
和OuterNode
对象返回true
。关于
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
对象,以便从一个Rule
到另一个Rule
中保留Adapter
的状态。一旦整个树形结构已经通过,就会调用Wrapper::commit()
,并且每个聚合的Adapter
对象将根据需要对原始Request
应用其状态。然后我们得到了一系列
Command
对象和修改后的Request
数组。
到底有什么意义?
嗯,我不想重新创建许多PHP框架/应用程序中常见的典型“路由表”,所以我选择了“路由树”。通过允许任意规则,您可以快速创建并附加一个AuthRule
(例如)到一个Node
,并且没有通过AuthRule
就不能访问整个分支。在理论上(在我的头脑中),它像一个神奇的独角兽,防止代码重复,并强制执行区域/模块组织。在实践中,我感到困惑和害怕。
为什么留下这堵无意义的墙?
好吧,这是我需要解决LSP问题的实现。每个Rule
对应一个Adapter
,这不好。我想保留每个Rule
之间的关系,以确保在构建树等时保持类型安全,但是我无法在抽象的Rule
中声明关键方法(evaluate()
),因为子类型的签名会发生变化。