После создания MVC-фреймворка в PHP я столкнулся с проблемой, которую можно было легко решить с помощью дженериков стиля Java. Абстрактный класс Controller может выглядеть примерно так:
abstract class Controller { abstract public function addModel(Model $model);
Может быть случай, когда подкласс класса Controller должен принимать только подкласс модели. Например, 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 нет поддержки дженериков. Есть ли какое-либо решение, которое все еще будет придерживаться принципов ООП?
Редактировать Я знаю, что PHP не требует типа намека на всех, но это, возможно, плохой ООП. Во-первых, из интерфейса (подписи метода) не очевидно, какие объекты должны быть приняты. Поэтому, если другой разработчик захочет использовать этот метод, должно быть очевидно, что объекты типа X требуются без необходимости просмотра реализации (тела метода), которая является плохим инкапсулированием и нарушает принцип скрытия информации. Во-вторых, поскольку нет безопасности типа, метод может принимать любую недопустимую переменную, что означает, что ручная проверка типа и бросок исключений необходимы повсюду!
Кажется, он работает для меня (хотя он и бросает строгое предупреждение) со следующим тестовым примером:
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);
Одна вещь, которую вы должны иметь в виду, однако, это строго не лучшая практика в терминах ООП. Если суперкласс может принимать объекты определенного класса в качестве аргумента метода, то все его подклассы также должны иметь возможность принимать объекты этого класса. Предотвращение подклассов из классов обработки, которые может принимать суперкласс, означает, что вы не можете использовать подкласс вместо суперкласса и быть на 100% уверенным, что он будет работать во всех случаях. Соответствующая практика известна как Принцип замещения Лискова и в ней говорится, что среди аргументов метода аргументы метода могут только ослабевать в подклассах, а тип возвращаемых значений может только усилиться (ввод может быть только более общим, вывод может только уточните).
Это очень разочаровывающая проблема, и я много раз обманывал ее, поэтому, если игнорировать ее в конкретном случае – это лучше всего, я предлагаю вам игнорировать ее. Но не делайте привычки к этому, или ваш код начнет развить всевозможные тонкие взаимозависимости, которые станут кошмаром для отладки (модульное тестирование не будет их улавливать, потому что отдельные единицы будут вести себя так, как ожидалось, это взаимодействие между ними где проблема). Если вы проигнорируете это, прокомментируйте код, чтобы сообщить другим об этом и что это преднамеренный выбор дизайна.
Независимо от того, какой изобретательный мир Java не всегда должен быть прав. Я думаю, что я обнаружил нарушение принципа замены Лискова здесь, и PHP прав, жалуясь на него в режиме E_STRICT:
Cite Wikipedia: «Если S является подтипом T, то объекты типа T в программе могут быть заменены объектами типа S без изменения каких-либо желательных свойств этой программы».
T – ваш контроллер. S – ваш ExtendedController. Вы должны иметь возможность использовать ExtendedController в каждом месте, где работает контроллер, не нарушая ничего. Изменение типа type на методе addModel () ломает вещи, потому что во всех местах, передающих объект типа Model, typehint теперь предотвратит передачу одного и того же объекта, если это не случайно ReOrderableModel.
Как избежать этого?
Ваш ExtendedController может оставить typehint как есть и проверить, получил ли он экземпляр ReOrderableModel или нет. Это обходит жалобы PHP, но все же ломает ситуацию в терминах замены Лискова.
Лучше всего создать новый метод addReOrderableModel()
предназначенный для ввода объектов ReOrderableModel в ExtendedController. Этот метод может иметь нужный тип, и может внутренне просто вызвать addModel()
чтобы поставить модель на место, где она ожидается.
Если вам требуется использовать ExtendedController вместо параметра Controller as, вы знаете, что ваш метод добавления ReOrderableModel присутствует и может использоваться. Вы явно заявляете, что Контроллер не будет соответствовать этому случаю. Каждый метод, который ожидает, что контроллер будет передан, не будет ожидать, что addReOrderableModel()
будет существовать и никогда не будет пытаться его вызвать. Каждый метод, который ожидает, что ExtendedController имеет право вызвать этот метод, потому что он должен быть там.
class ExtendedController extends Controller { public function addReOrderableModel(ReOrderableModel $model) { return $this->addModel($model); } }
Мое обходное решение заключается в следующем:
/** * 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());
эта/** * 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());
не/** * 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-аналог), чем большинство из них.
Я надеюсь, что некоторые из вас посчитают это полезным.
Любые улучшения в этом дизайне приветствуются!
Раньше я сталкивался с такими же проблемами. И я использовал что-то подобное, чтобы справиться с этим.
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 } }
Вы можете переключиться на Hack и HHVM. Он разработан Facebook и полностью совместим с PHP. Вы можете использовать <?php
или <?hh
Он поддерживает то, что вы хотите:
http://docs.hhvm.com/manual/en/hack.generics.php
Я знаю, что это не PHP. Но он совместим с ним, а также значительно улучшает вашу производительность.
Вы можете сделать это грязно, передав этот тип в качестве второго аргумента конструктора
<?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(); } }
Чтобы обеспечить высокий уровень статического анализа кода, строгое типирование и удобство использования, я придумал это решение: 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 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.