PHP对Java风格的类泛型有答案吗?

2022-08-30 11:43:43

在PHP中构建MVC框架时,我遇到了一个问题,可以使用Java风格的泛型轻松解决。抽象的 Controller 类可能如下所示:

abstract class Controller {

abstract public function addModel(Model $model);

可能存在这样一种情况,即类 Controller 的子类应仅接受 Model 的子类。例如,ExtendedController 应该只接受 ReOrderableModel 到 addModel 方法中,因为它提供了一个 reOrder() 方法,ExtendedController 需要访问该方法:

class ExtendedController extends Controller {

public function addModel(ReOrderableModel $model) {

在 PHP 中,继承的方法签名必须完全相同,因此不能将类型提示更改为其他类,即使该类继承了超类中提示的类类型也是如此。在java中,我会简单地这样做:

abstract class Controller<T> {

abstract public addModel(T model);


class ExtendedController extends Controller<ReOrderableModel> {

public addModel(ReOrderableModel model) {

但是 PHP 中没有泛型支持。是否有任何解决方案仍然遵循 OOP 原则?

编辑我知道PHP根本不需要类型提示,但它可能是糟糕的OOP。首先,从接口(方法签名)中看不出应该接受哪种对象。因此,如果另一个开发人员想要使用该方法,那么很明显,需要X类型的对象,而不必查看实现(方法体),这是糟糕的封装并破坏了信息隐藏原则。其次,由于没有类型安全性,该方法可以接受任何无效变量,这意味着需要手动类型检查和异常抛出!


答案 1

它似乎适用于我(尽管它确实抛出了严格的警告),具有以下测试用例:

class PassMeIn
{

}

class PassMeInSubClass extends PassMeIn
{

}

class ClassProcessor
{
    public function processClass (PassMeIn $class)
    {
        var_dump (get_class ($class));
    }
}

class ClassProcessorSubClass extends ClassProcessor 
{
    public function processClass (PassMeInSubClass $class)
    {
        parent::processClass ($class);
    }
}

$a  = new PassMeIn;
$b  = new PassMeInSubClass;
$c  = new ClassProcessor;
$d  = new ClassProcessorSubClass;

$c -> processClass ($a);
$c -> processClass ($b);
$d -> processClass ($b);

如果严格的警告是你真正不想要的,你可以像这样解决它。

class ClassProcessor
{
    public function processClass (PassMeIn $class)
    {
        var_dump (get_class ($class));
    }
}

class ClassProcessorSubClass extends ClassProcessor 
{
    public function processClass (PassMeIn $class)
    {
        if ($class instanceof PassMeInSubClass)
        {
            parent::processClass ($class);
        }
        else
        {
            throw new InvalidArgumentException;
        }
    }
}

$a  = new PassMeIn;
$b  = new PassMeInSubClass;
$c  = new ClassProcessor;
$d  = new ClassProcessorSubClass;

$c -> processClass ($a);
$c -> processClass ($b);
$d -> processClass ($b);
$d -> processClass ($a);

但是,您应该记住的一件事是,这在OOP术语中严格来说不是最佳实践。如果一个超类可以接受特定类的对象作为方法参数,那么它的所有子类也应该能够接受该类的对象。阻止子类处理超类可以接受的类意味着您不能使用子类代替超类,并且 100% 确信它将在所有情况下都有效。相关的实践被称为Liskov替换原理,它指出,除其他外,方法参数的类型只能在子类中变弱,返回值的类型只能变得更强(输入只能变得更一般,输出只能变得更具体)。

这是一个非常令人沮丧的问题,我自己已经多次反对它,所以如果在特定情况下忽略它是最好的办法,那么我建议你忽略它。但是不要养成习惯,否则你的代码将开始开发各种微妙的相互依赖关系,这将是调试的噩梦(单元测试不会抓住它们,因为各个单元的行为会像预期的那样,问题是它们之间的交互)。如果您忽略它,请注释代码以让其他人知道它,并且这是一个深思熟虑的设计选择。


答案 2

无论Java世界发明了什么,都不需要总是正确的。我认为我在这里发现了对Liskov替换原则的违反,PHP在E_STRICT模式下抱怨它是正确的:

引用维基百科:“如果S是T的子类型,那么程序中T类型的对象可以被替换为S类型的对象,而不会改变该程序的任何理想属性。

T 是您的控制器。S 是您的扩展控制器。您应该能够在控制器工作的每个位置使用扩展控制器,而不会破坏任何东西。更改 addModel() 方法上的 typehint 会破坏一些问题,因为在传递 Model 类型的对象的每个位置,如果它不是意外的 ReOrderableModel,则 typehint 现在将阻止传递相同的对象。

如何逃脱这个?

您的扩展控制器可以保持键入说明不变,并在之后检查他是否获得了ReOrderableModel的实例。这规避了PHP的抱怨,但它仍然在Liskov替换方面打破了一切。

更好的方法是创建一个新方法,旨在将 ReOrderableModel 对象注入 ExtendedController。此方法可以具有所需的类型隐藏,并且可以在内部调用以将模型放在预期的位置。addReOrderableModel()addModel()

如果您需要使用扩展控制器而不是控制器作为参数,则您知道添加ReOrderableModel的方法已经存在并且可以使用。您明确声明控制器不适合这种情况。每个期望传递控制器的方法都不会期望存在,并且永远不会尝试调用它。每个需要扩展控制器的方法都有权调用此方法,因为它必须存在。addReOrderableModel()

class ExtendedController extends Controller
{
  public function addReOrderableModel(ReOrderableModel $model)
  {
    return $this->addModel($model);
  }
}

推荐