PHP是否有Java样式类泛型的答案?

Jon*_*han 33 php java oop generics inheritance

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

abstract class Controller {

abstract public function addModel(Model $model);
Run Code Online (Sandbox Code Playgroud)

可能存在类Controller的子类只应接受Model的子类的情况.例如,ExtendedController应该只接受ReOrderableModel到addModel方法中,因为它提供了一个reRerder()方法,ExtendedController需要有权访问:

class ExtendedController extends Controller {

public function addModel(ReOrderableModel $model) {
Run Code Online (Sandbox Code Playgroud)

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

abstract class Controller<T> {

abstract public addModel(T model);


class ExtendedController extends Controller<ReOrderableModel> {

public addModel(ReOrderableModel model) {
Run Code Online (Sandbox Code Playgroud)

但PHP中没有泛型支持.有没有任何解决方案仍然符合OOP原则?

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

Gor*_*onM 12

它似乎适用于我(虽然它确实抛出严格的警告)与以下测试用例:

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);
Run Code Online (Sandbox Code Playgroud)

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

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);
Run Code Online (Sandbox Code Playgroud)

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

这是一个非常令人沮丧的问题,我自己已经多次反对它,所以如果在特定情况下忽略它是最好的事情,那么我建议你忽略它.但是不要养成它的习惯,否则你的代码会开始形成各种微妙的相互依赖性,这将是调试的噩梦(单元测试不会捕获它们,因为单个单元将按预期运行,这是它们之间的相互作用问题出在哪里).如果你忽略它,那么评论代码让别人知道它,这是一个刻意的设计选择.


Pow*_*ave 8

我的解决方法如下:

/**
 * Generic list logic and an abstract type validator method.
 */    
abstract class AbstractList {
    protected $elements;

    public function __construct() {
        $this->elements = array();
    }

    public function add($element) {
        $this->validateType($element);
        $this->elements[] = $element;
    }

    public function get($index) {
        if ($index >= sizeof($this->elements)) {
            throw new OutOfBoundsException();
        }
        return $this->elements[$index];
    }

    public function size() {
        return sizeof($this->elements);
    }

    public function remove($element) {
        validateType($element);
        for ($i = 0; $i < sizeof($this->elements); $i++) {
            if ($this->elements[$i] == $element) {
               unset($this->elements[$i]);
            }
        }
    }

    protected abstract function validateType($element);
}


/**
 * Extends the abstract list with the type-specific validation
 */
class MyTypeList extends AbstractList {
    protected function validateType($element) {
        if (!($element instanceof MyType)) {
            throw new InvalidArgumentException("Parameter must be MyType instance");
        }
    }
}

/**
 * Just an example class as a subject to validation.
 */
class MyType {
    // blahblahblah
}


function proofOfConcept(AbstractList $lst) {
    $lst->add(new MyType());
    $lst->add("wrong type"); // Should throw IAE
}

proofOfConcept(new MyTypeList());
Run Code Online (Sandbox Code Playgroud)

虽然这仍然与Java泛型不同,但它几乎最小化了模仿行为所需的额外代码.

此外,它比其他人给出的一些例子更多一些代码,但是 - 至少对我而言 - 它似乎比大多数代码更干净(并且对Java来说更简洁).

我希望你们中的一些人会发现它很有用.

欢迎对此设计进行任何改进!


Sve*_*ven 8

无论Java世界发明什么,都不一定总是正确的.我认为我在这里检测到违反了Liskov替换原则,并且PHP在E_STRICT模式中抱怨它是正确的:

引用维基百科:"如果S是T的子类型,那么程序中类型T的对象可以用类型S的对象替换,而不会改变该程序的任何所需属性."

T是你的控制器.S是你的ExtendedController.您应该能够在Controller工作的每个地方使用ExtendedController而不会破坏任何内容.更改addModel()方法上的typehint会破坏一些东西,因为在传递类型为Model的对象的每个地方,如果不是意外的ReOrderableModel,typehint现在会阻止传递同一个对象.

怎么逃避这个?

您的ExtendedController可以按原样保留typehint,然后检查他是否获得了ReOrderableModel的实例.这避免了PHP投诉,但它仍然在Liskov替换方面打破了局面.

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

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

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