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 替换原则,并导致类型安全问题。船尾。
问题
好的,所以这行不通。但是,在这种情况下,存在哪些模式或策略来克服类型安全问题和LSP违规,但仍保持SpecializedConsumer
to的类型关系Consumer
?
我认为可以将答案提炼为“ ya dun goofed, back to the drawing board ”是完全可以接受的。
注意事项、详细信息和勘误表
好的,一个直接的解决方案显示为“不要在中定义
consume()
方法Consumer
”。好的,这是有道理的,因为方法声明与签名一样好。从语义上讲,尽管没有consume()
,即使有一个未知的参数列表,也会让我的大脑有点受伤。也许有更好的方法。从我正在阅读的内容来看,很少有语言支持参数类型协方差;PHP 就是其中之一,并且是这里的实现语言。更复杂的是,我看到了涉及泛型的创造性“解决方案” ;PHP 不支持的另一个特性。
来自 Wiki's Variance (computer science) - Need for covariant argument types?:
这在某些情况下会产生问题,其中参数类型应该是协变的以模拟现实生活中的需求。假设你有一个代表一个人的类。一个人可以看医生,所以这个类可能有一个方法 virtual void
Person::see(Doctor d)
。现在假设您要创建该类的子Person
类Child
. 也就是说,aChild
是一个人。然后可能想创建一个 , 的子Doctor
类Pediatrician
。如果孩子只看儿科医生,我们希望在类型系统中强制执行。但是,一个简单的实现会失败:因为 aChild
是 aPerson
,所以Child::see(d)
必须采用 anyDoctor
,而不仅仅是 aPediatrician
。文章接着说:
在这种情况下,可以使用访问者模式来强制执行这种关系。在 C++ 中解决问题的另一种方法是使用泛型编程。
同样,可以创造性地使用泛型来解决问题。我正在探索访问者模式,因为无论如何我都有一个半生不熟的实现,但是文章中描述的大多数实现都利用了方法重载,这是 PHP 中另一个不受支持的特性。
<too-much-information>
执行
由于最近的讨论,我将扩展我忽略的具体实现细节(如,我可能会包括太多)。
为简洁起见,我已经排除了那些(应该)目的非常明确的方法体。我试图保持简短,但我倾向于罗嗦。我不想倾倒一堵代码,所以解释跟随/在代码块之前。如果您有编辑权限,并且想要清理它,请执行此操作。此外,代码块不是来自项目的复制粘贴。如果某些事情没有意义,它可能不会;冲我大喊澄清。
关于原来的问题,以后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()
),或者简单地分别返回true
、 forInnerNode
和OuterNode
对象。
至于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);
}
因此,给定的 user-landRule
接受预期的 user-land Adapter
。如果Adapter
需要有关请求的信息,则将其路由通过Wrapper
,以保持原始的完整性Request
。
作为Wrapper
聚合Adapter
对象,它将现有实例传递给后续Rule
对象,以便从一个到下一个Adapter
保存一个状态。Rule
一旦整个树通过,Wrapper::commit()
就会被调用,并且每个聚合Adapter
对象将根据需要将其状态应用于原始Request
.
然后我们得到一个对象数组Command
和一个修改后的Request
.
到底有什么意义?
好吧,我不想重新创建许多 PHP 框架/应用程序中常见的原型“路由表”,所以我选择了“路由树”。通过允许任意规则,您可以快速创建一个AuthRule
(例如)并将其附加到一个Node
,并且不再是整个分支都可以在不通过AuthRule
. 理论上(在我的脑海中)它就像一个神奇的独角兽,防止代码重复,并执行区域/模块组织。在实践中,我感到困惑和害怕。
为什么我离开了这堵胡说八道的墙?
嗯,这是我需要解决 LSP 问题的实现。每个都Rule
对应一个Adapter
,这不好。我想保留 each 之间的关系Rule
,以确保构造树时的类型安全等,但是我不能evaluate()
在 abstract 中声明 key 方法 ( ) Rule
,因为子类型的签名会发生变化。
另一方面,我正在整理Adapter
创建/管理方案;是否是Rule
创建它的责任,等等。
</too-much-information>