【问题标题】:Does PHP have an answer to Java style class generics?PHP 对 Java 风格的类泛型有答案吗?
【发布时间】:2012-03-27 15:10:33
【问题描述】:

在用 PHP 构建 MVC 框架时,我遇到了一个可以使用 Java 样式泛型轻松解决的问题。一个抽象的 Controller 类可能看起来像这样:

abstract class Controller {

abstract public function addModel(Model $model);

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

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 类型的对象,而不必查看封装不良并违反信息隐藏原则的实现(方法体)。其次,因为没有类型安全,该方法可以接受任何无效变量,这意味着需要手动进行类型检查和异常抛出!

【问题讨论】:

  • 在php中你可以传递任何你想要的类型对象而不必担心
  • 我已经意识到这一点,但这可能是糟糕的 OOP。首先,从接口(方法签名)来看,应该接受什么样的对象并不明显。因此,如果另一个开发人员想要使用该方法,那么显然应该只使用 X 类型的对象,而不必查看封装不良并违反信息隐藏原则的实现(方法体)。其次,由于没有类型安全,该方法可以接受任何无效变量,这意味着需要手动进行类型检查和异常抛出。
  • 这也是糟糕的 OOP,因为它违反了 LSP,您的 ExtendedController 专门化 控制器而不是扩展它。如果父级上的addModel 可以接受需要子类addModel 只接受ReOderableModel 的模型,这是非常糟糕的oop,并且违反了LSP。它也不会以任何明智的方式在 Java 中工作。这是 2 年前的,所以你现在可能已经知道了这一切……
  • 请注意,有一个RFC for generics,目前正在草稿中。

标签: java php oop generics inheritance


【解决方案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 Substitution Principle,它指出,除其他外,方法参数的类型只能在子类中变得更弱,返回值的类型只能变得更强(输入只能变得更一般,输出可以只会变得更具体)。

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

【讨论】:

  • 你知道吗?我什至没有尝试看看它是否真的有效哈哈。我只是读到它不起作用并相信它。我想我可以使用 @ 符号来抑制警告。你能想到为什么这将是一个非常糟糕的主意的任何原因吗?要么是那个,要么是你下面的第二个解决方案。
  • 一般来说,您真的很想避免使用@ 运算符,因为它基本上相当于将错误消息扫到地毯下。在这种特殊情况下,它可能不会导致长期问题,但您不能指望这一点,因为 E_STRICT 警告表明该语言可能会在某个时候更改为不再支持您正在做的事情。我不确定您是否可以使用它来抑制方法声明中的错误。您可以设置一个不包括 E_STRICT 的 error_reporting 值,尽管这只是在掩盖问题。
  • PHP 的手册在 E_STRICT 上有这样的说法:In PHP 5 a new error level E_STRICT is available. As E_STRICT is not included within E_ALL you have to explicitly enable this kind of error level. Enabling E_STRICT during development has some benefits. STRICT messages will help you to use the latest and greatest suggested method of coding, for example warn you about using deprecated functions. 所以我想说最好的方法是在您的生产服务器上关闭 E_STRICT,但在您的开发/测试机器上将其保留。
  • +1 用于详细说明 Liskov 替换原则的一个方面。
【解决方案2】:

你可以通过将类型作为构造函数的第二个参数传递来做这件事

<?php class Collection implements IteratorAggregate{
      private $type;
      private $container;
      public function __construct(array $collection, $type='Object'){
          $this->type = $type;
          foreach($collection as $value){
             if(!($value instanceof $this->type)){
                 throw new RuntimeException('bad type for your collection');
             }  
          }
          $this->container = new \ArrayObject($collection);
      }
      public function getIterator(){
         return $this->container->getIterator();
      }
    }

【讨论】:

  • 这是一个有趣的解决方案!与 Starx 的解决方案类似,只是类作为参数 nice 传递。
【解决方案3】:

我之前确实遇到过同样类型的问题。我用这样的东西来解决它。

Class Myclass {

    $objectParent = "MyMainParent"; //Define the interface or abstract class or the main parent class here
    public function method($classObject) {
        if(!$classObject instanceof $this -> objectParent) { //check 
             throw new Exception("Invalid Class Identified");
        }
        // Carry on with the function
    }

}

【讨论】:

  • 创意解决方案!我可能会结合类型提示来执行此操作。所以它类型检查超类,然后手动检查子类。
  • @jonathan,如果您对超类进行类型转换,则传递的对象必须是该类的对象。这正是为什么我的解决方案不使用它,而是稍后检查实例以验证它的原因。
  • 我的意思是 addModel(Model $model) 会检查它是否是一个模型,但是在方法体中我可以通过 if($model instanceof ReOrderableModel) 手动检查。 ReOrderableModel 扩展了 Model ,因此可以通过 addModel(Model $model) 方法实现。你是说这行不通吗?
【解决方案4】:

我的解决方法如下:

/**
 * 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());

虽然这仍然与 Java 泛型不同,但它几乎最大限度地减少了模仿行为所需的额外代码。

此外,它的代码比其他人给出的一些示例要多一些,但是 - 至少对我而言 - 它似乎比大多数示例更干净(并且与 Java 对应物更相似)。

我希望你们中的一些人会觉得它有用。

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

【讨论】:

  • 我猜你应该已经将代码添加到 PHP 的 SPL 类之一,如 ArrayObject 或 SplDoublyLinkedList
  • @Sven 好吧,是的。此外,它显然不是 Java List&lt;?&gt; 提供的完整接口,而是最常用的子集。它还可以通过引入实现验证器的AbstractValidatedList 类来进一步改进,但调用子级以获取返回类名的抽象getElementType() 方法。我对 PHP 的使用有点过时了,因为我去年几乎只使用 J2EE。感谢您指出问题!
  • 我认为问题不在于列表。这是关于 PHP 中的继承。
  • @Sven 嗯,基本概念可以随意应用到其他类。与 Java 泛型相比的主要区别在于,您会遇到运行时错误,但没有 IDE 会抱怨类型不匹配。
  • @Sven 泛型类在编译时不会解析为单独的具体类,但目的是不必使用 Object 作为主体类型,这会从代码中删除类型安全性。相反,以类似模板的方式,您可以指定特定实例应使用的实际类型。它们被解析为具有对主题类的引用的同一类;几乎包裹它。这在强类型环境中提供了高代码可重用性。尽管 PHP 不是其中之一,但 OOP 本身具有类型化性质,因此可能会出现对此类功能的需求。
【解决方案5】:

Java 世界发明的任何东西都不一定总是正确的。我想我在这里检测到违反了 Liskov 替换原则,PHP 在 E_STRICT 模式下抱怨它是正确的:

引用 Wikipedia:“如果 S 是 T 的子类型,则程序中类型 T 的对象可以替换为类型 S 的对象,而不会改变该程序的任何所需属性。”

T 是你的控制器。 S 是您的扩展控制器。您应该能够在控制器工作的每个地方使用 ExtendedController 而不会破坏任何东西。更改 addModel() 方法上的类型提示会破坏事情,因为在每个传递 Model 类型对象的地方,如果它不是意外的 ReOrderableModel,那么类型提示现在将阻止传递相同的对象。

如何逃脱?

您的 ExtendedController 可以保持类型提示不变,然后检查他是否获得了 ReOrderableModel 的实例。这绕过了 PHP 的抱怨,但它仍然破坏了 Liskov 替换方面的内容。

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

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

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

【讨论】:

    【解决方案6】:

    您可以考虑切换到 Hack 和 HHVM。它由 Facebook 开发,完全兼容 PHP。您可以决定使用&lt;?php&lt;?hh

    它支持你想要的:

    http://docs.hhvm.com/manual/en/hack.generics.php

    我知道这不是 PHP。但它与它兼容,并且还可以显着提高您的性能。

    【讨论】:

    • 它实际上并不完全兼容 PHP。有很多微小的东西是不兼容的,但总体上却有很大的不同。
    【解决方案7】:

    为了提供高水平的静态代码分析、严格的输入和可用性,我想出了这个解决方案:https://gist.github.com/rickhub/aa6cb712990041480b11d5624a60b53b

    /**
     * Class GenericCollection
     */
    class GenericCollection implements \IteratorAggregate, \ArrayAccess{
        /**
         * @var string
         */
        private $type;
    
        /**
         * @var array
         */
        private $items = [];
    
        /**
         * GenericCollection constructor.
         *
         * @param string $type
         */
        public function __construct(string $type){
            $this->type = $type;
        }
    
        /**
         * @param $item
         *
         * @return bool
         */
        protected function checkType($item): bool{
            $type = $this->getType();
            return $item instanceof $type;
        }
    
        /**
         * @return string
         */
        public function getType(): string{
            return $this->type;
        }
    
        /**
         * @param string $type
         *
         * @return bool
         */
        public function isType(string $type): bool{
            return $this->type === $type;
        }
    
        #region IteratorAggregate
    
        /**
         * @return \Traversable|$type
         */
        public function getIterator(): \Traversable{
            return new \ArrayIterator($this->items);
        }
    
        #endregion
    
        #region ArrayAccess
    
        /**
         * @param mixed $offset
         *
         * @return bool
         */
        public function offsetExists($offset){
            return isset($this->items[$offset]);
        }
    
        /**
         * @param mixed $offset
         *
         * @return mixed|null
         */
        public function offsetGet($offset){
            return isset($this->items[$offset]) ? $this->items[$offset] : null;
        }
    
        /**
         * @param mixed $offset
         * @param mixed $item
         */
        public function offsetSet($offset, $item){
            if(!$this->checkType($item)){
                throw new \InvalidArgumentException('invalid type');
            }
            $offset !== null ? $this->items[$offset] = $item : $this->items[] = $item;
        }
    
        /**
         * @param mixed $offset
         */
        public function offsetUnset($offset){
            unset($this->items[$offset]);
        }
    
        #endregion
    }
    
    
    /**
     * Class Item
     */
    class Item{
        /**
         * @var int
         */
        public $id = null;
    
        /**
         * @var string
         */
        public $data = null;
    
        /**
         * Item constructor.
         *
         * @param int    $id
         * @param string $data
         */
        public function __construct(int $id, string $data){
            $this->id = $id;
            $this->data = $data;
        }
    }
    
    
    /**
     * Class ItemCollection
     */
    class ItemCollection extends GenericCollection{
        /**
         * ItemCollection constructor.
         */
        public function __construct(){
            parent::__construct(Item::class);
        }
    
        /**
         * @return \Traversable|Item[]
         */
        public function getIterator(): \Traversable{
            return parent::getIterator();
        }
    }
    
    
    /**
     * Class ExampleService
     */
    class ExampleService{
        /**
         * @var ItemCollection
         */
        private $items = null;
    
        /**
         * SomeService constructor.
         *
         * @param ItemCollection $items
         */
        public function __construct(ItemCollection $items){
            $this->items = $items;
        }
    
        /**
         * @return void
         */
        public function list(){
            foreach($this->items as $item){
                echo $item->data;
            }
        }
    }
    
    
    /**
     * Usage
     */
    $collection = new ItemCollection;
    $collection[] = new Item(1, 'foo');
    $collection[] = new Item(2, 'bar');
    $collection[] = new Item(3, 'foobar');
    
    $collection[] = 42; // InvalidArgumentException: invalid type
    
    $service = new ExampleService($collection);
    $service->list();
    

    即使这样的感觉会好很多:

    class ExampleService{
        public function __construct(Collection<Item> $items){
            // ..
        }
    }
    

    希望泛型能尽快进入 PHP。

    【讨论】:

      【解决方案8】:

      另一种选择是 splat 运算符 + 类型化提示 + 私有数组的组合

      <?php
      
      class Student {
          
          public string $name;
          public function __construct(string $name){
              $this->name = $name;
          }
          
      }
      
      class Classe {
          
          private $students = [];
          
          public function add(Student ...$student){
              array_merge($this->students, $student);
          }
          
          public function getAll(){
              return $this->students;
          }
          
      }
      
      $c = new Classe();
      $c->add(new Student('John'), new Student('Mary'), new Student('Kate'));
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-01-20
        • 1970-01-01
        • 2015-01-13
        • 1970-01-01
        • 2010-11-07
        • 1970-01-01
        相关资源
        最近更新 更多