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,但仍然保持 SpecializedConsumer
到 Consumer
的类型关系?
我想完全可以接受的是,答案可以被提炼为“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
类是 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
,分别用于 InnerNode
和 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);
}
所以给定的用户土地 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 回答
我所知道的唯一一个是DIY策略:在函数定义中接受简单的
Argument
并立即检查它是否足够专业:要正确回答这个问题,我们必须退后一步,看看你试图以更一般的方式解决的问题(你的问题已经非常普遍) .
真正的问题
真正的问题是你试图使用继承来解决业务逻辑问题 . 由于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(在您抽象不同的子系统的情况下)专业消费者)......
简而言之,这是一个难以回答的问题,因为有太多的解决方案,很难说哪个是正确的......