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

时间:2012-03-10 12:26:42

标签: java php oop generics inheritance

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

8 个答案:

答案 0 :(得分: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);

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

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并且它声明,除其他外,方法参数的类型只能在子类中变弱,并且返回值的类型只能变得更强(输入只能变得更通用,输出只能更具体)。

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

答案 1 :(得分: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());

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

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

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

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

答案 2 :(得分: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);
  }
}

答案 3 :(得分: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
    }

}

答案 4 :(得分:3)

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

它支持你想要的东西:

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

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

答案 5 :(得分: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();
      }
    }

答案 6 :(得分:1)

为了提供高水平的静态代码分析,严格的输入和可用性,我提出了这个解决方案: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。

答案 7 :(得分:0)

一种替代方法是 splat运算符+输入的提示+私有数组的组合:

function Car(props) {
  const [color, setColor] = useState(props.color);

  return (
        <div>
      <p>
        I have a {props.year + " " + color + " " + props.brand + " " + 
        props.model}
      </p>  
      <button type="button" onClick={() => {
  setColor('blue')
}}>
        Changecolor
      </button>        
    </div>
  );
}