我正在用PHP编写一个框架,并且遇到了一个闻起来很糟糕的模式。我似乎实施了违反Liskov替代原则(LSP)的合同(q.v. Design By Contract)。由于原始示例被大量抽象化,我将其置于现实世界的背景中:
(n.b。我不是引擎/车辆/扫帚 - 扫帚的人,请原谅我,如果它不切实际)
假设我们有车辆的贫血抽象类,我们还有两种子类型的车辆 - 可以加油的车辆和那些不能加油的车辆(例如推车)。对于这个例子,我们只关注可加燃料类型:
abstract class AbstractVehicle {}
abstract class AbstractFuelledVehicle extends AbstractVehicle
{
private $lastRefuelPrice;
final public function refuelVehicle(FuelInterface $fuel)
{
$this->checkFuelType($fuel);
$this->lastRefuelPrice = $fuel->getCostPerLitre;
}
abstract protected function checkFuelType(FuelInterface $fuel);
}
abstract class AbstractNonFuelledVehicle extends AbstractVehicle { /* ... */ }
现在,让我们来看看"燃料"类:
abstract class AbstractFuel implements FuelInterface
{
private $costPerLitre;
final public function __construct($costPerLitre)
{
$this->costPerLitre = $costPerLitre;
}
final public function getCostPerLitre()
{
return $this->costPerLitre;
}
}
interface FuelInterface
{
public function getCostPerLitre();
}
这就是完成的所有抽象类,现在让我们看一下具体的实现。首先,两个具体的燃料实现,包括一些贫血界面,以便我们可以正确地打字/嗅探它们:
interface MotorVehicleFuelInterface {}
interface AviationFuelInterface {}
final class UnleadedPetrol extends AbstractFuel implements MotorVehicleFuelInterface {}
final class AvGas extends AbstractFuel implements AviationFuelInterface {}
现在最后,我们有车辆的具体实施,确保正确的燃料类型(接口)用于加油特定的车辆类别,如果不相容则抛出异常:
class Car extends AbstractFuelledVehicle
{
final protected function checkFuelType(FuelInterface $fuel)
{
if(!($fuel instanceof MotorVehicleFuelInterface))
{
throw new Exception('You can only refuel a car with motor vehicle fuel');
}
}
}
class Jet extends AbstractFuelledVehicle
{
final protected function checkFuelType(FuelInterface $fuel)
{
if(!($fuel instanceof AviationFuelInterface))
{
throw new Exception('You can only refuel a jet with aviation fuel');
}
}
}
Car和Jet都是AbstractFuelledVehicle的子类型,因此根据LSP,我们应该能够替换它们。
由于如果提供了错误的AbstractFuel子类型,checkFuelType()会抛出异常,这意味着如果我们将AbstractFuelledVehicle子类型Car替换为Jet(反之亦然)而不替换相关的燃料子类型,我们将触发异常。
这是:
答案 0 :(得分:1)
将评论合并到答案中......
我同意LSP的分析:原始版本是违规的,我们总是可以通过弱化层次结构顶部的合同来解决LSP违规问题。但是,我不认为这是一个优雅的解决方案。类型检查始终是代码气味(在OOP中)。用OP自己的话说,“ ...包括一些贫血界面,以便我们可以打字/嗅探它们...... ”这里被嗅到的是糟糕设计的恶臭。
我的观点是LSP是最不重要的问题; instanceof
是一个OO code smell。这里的LSP合规就像腐烂的房子上的新鲜油漆:它可能看起来很漂亮,但基础仍然根本不健全。从设计中消除类型检查。只有这样担心LSP。
一般而言,OO设计的SOLID原则,特别是LSP,作为实际上面向对象的设计的一部分是最有效的。在OOP中,类型检查由多态性替换。
答案 1 :(得分:0)
再想一想,我认为 是对Liskov替代原则的技术违反。一种重新定义LSP的方法是" 一个子类应该不再需要,并且承诺"。在这种情况下,Car和Jet混凝土类都需要特定类型的燃料才能继续执行代码(这违反了LSP),另外还有方法 checkFuelType ()可以被覆盖以包括各种奇怪和奇妙的行为。我认为更好的方法是:
更改AbstractFuelledVehicle类以在加油前检查燃油类型:
abstract class AbstractFuelledVehicle extends AbstractVehicle
{
private $lastRefuelPrice;
final public function refuelVehicle(FuelInterface $fuel)
{
if($this->isFuelCompatible($fuel))
{
$this->lastRefuelPrice = $fuel->getCostPerLitre;
} else {
/*
Trigger some kind of warning here,
whether externally via a message to the user
or internally via an Exception
*/
}
}
/** @return bool */
abstract protected function isFuelCompatible(FuelInterface $fuel);
}
对我来说,这是一个更优雅的解决方案,并没有任何代码味道。我们可以将燃料从UnleadedPetrol交换到AvGas,并且超类的行为保持不变,尽管有两种可能的结果(即它的行为不由具体类决定它,可以抛出异常,记录错误,跳过跳汰机等等)