PHPでMVCフレームワークを構築すると、Javaスタイルのジェネリックを使用して簡単に解決できる問題に遭遇しました。抽象Controllerクラスは次のようになります。
abstract class Controller {
abstract public function addModel(Model $model);
クラスControllerのサブクラスがModelのサブクラスのみを受け入れる必要がある場合があります。たとえば、ExtendedControllerは、ReedableableModelのみを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のオブジェクトが必要であることは明らかです。次に、型の安全性がないため、メソッドは無効な変数を受け入れることができます。これは、手動の型チェックと例外のスローが至る所で必要であることを意味します!
次のテストケースでは、(厳密な警告をスローしますが)私にとってはうまくいくようです。
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 であり、特に、メソッド引数のタイプはサブクラスでのみ弱くなり、戻り値のタイプはより強くなるだけです(入力はより一般的になり、出力はより詳細にのみ取得できます)。
これは非常にイライラする問題であり、私は何度も自分自身でそれをブラッシュアップしたので、特定のケースでそれを無視することが最善の場合は、無視することをお勧めします。しかし、それを習慣にしないでください。そうしないと、コードはデバッグするのに悪夢となるあらゆる種類の微妙な相互依存性を開発し始めます(個々のユニットが期待どおりに動作するため、ユニットテストはそれらを捕捉しません、それはそれらの間の相互作用です問題のある場所)。無視する場合は、コードをコメント化して他の人に知らせ、それが意図的な設計選択であることを知らせます。
Java世界が発明したものは常に正しい必要はありません。ここでLiskov置換原理の違反を検出したと思います。そして、PHP E_STRICTモードの場合:
Wikipediaを引用:「SがTのサブタイプである場合、プログラム内のタイプTのオブジェクトは、そのプログラムの望ましいプロパティを変更せずにタイプSのオブジェクトに置き換えることができます。」
Tはコントローラーです。 SはExtendedControllerです。 Controllerが機能するすべての場所で、何も壊さずにExtendedControllerを使用できるはずです。 addModel()メソッドでtypehintを変更すると、Model型のオブジェクトを渡したすべての場所で、誤ってReOrderableModelでない場合にtypehintが同じオブジェクトを渡せなくなるため、問題が発生します。
これを逃れる方法は?
ExtendedControllerはtypehintをそのままにして、後でReOrderableModelのインスタンスを取得したかどうかを確認できます。これはPHP苦情を回避しますが、それでもLiskov置換に関しては物事を壊します。
より良い方法は、ReOrderableModelオブジェクトをExtendedControllerに注入するように設計された新しいメソッドaddReOrderableModel()
を作成することです。このメソッドは、必要なtypehintを持つことができ、内部でaddModel()
を呼び出して、モデルを期待される場所に配置することができます。
パラメーターとしてコントローラーの代わりにExtendedControllerを使用する必要がある場合は、ReOrderableModelを追加するメソッドが存在し、使用できることがわかります。この場合、コントローラーが適合しないことを明示的に宣言します。 Controllerが渡されることを期待するすべてのメソッドは、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());
これはまだ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ではないことを知っています。しかし、それはそれと互換性があり、パフォーマンスも劇的に改善します。
コンストラクターの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();
}
}
高レベルの静的コード分析、厳密なタイピング、および使いやすさを提供するために、このソリューションを思いつきました: 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に入ることを願っています。