Изменение сигнатуры метода для реализации классов в PHP

9

Есть ли достойный обходной путь к отсутствию в Generics PHP, позволяющего статический контроль кода для выявления согласованности типов?

У меня есть абстрактный класс, который я хочу разделить на подклассы, а также обеспечить, чтобы один из методов изменился с получения параметра одного типа на параметр, который является подклассом этого параметра.

abstract class AbstractProcessor {
    abstract function processItem(Item $item);
}

class WoodProcessor extends AbstractProcessor {
    function processItem(WoodItem $item){}
}

Это не разрешено в PHP, потому что это изменяет сигнатуру методов, что недопустимо. С обобщением стиля Java вы можете сделать что-то вроде:

abstract class AbstractProcessor<T> {
    abstract function processItem(T $item);
}

class WoodProcessor extends AbstractProcessor<WoodItem> {
    function processItem(WoodItem $item);
}

Но, очевидно, PHP не поддерживает их.

Google для этой проблемы, люди предлагают использовать instanceofдля проверки ошибок во время выполнения, например

class WoodProcessor extends AbstractProcessor {
    function processItem(Item $item){
        if (!($item instanceof WoodItem)) {
            throw new \InvalidArgumentException(
                "item of class ".get_class($item)." is not a WoodItem");
        } 
    }
}

Но это работает только во время выполнения, оно не позволяет вам проверять ваш код на наличие ошибок с помощью статического анализа - так есть ли какой-нибудь разумный способ обработки этого в PHP?

Более полный пример проблемы:

class StoneItem extends Item{}
class WoodItem extends Item{}

class WoodProcessedItem extends ProcessedItem {
    function __construct(WoodItem $woodItem){}
}

class StoneProcessedItem extends ProcessedItem{
    function __construct(StoneItem $stoneItem){}
}

abstract class AbstractProcessor {
    abstract function processItem(Item $item);

    function processAndBoxItem(Box $box, Item $item) {
       $processedItem = $this->processItem($item);
       $box->insertItem($item);
    }

    //Lots of other functions that can call processItem
}

class WoodProcessor extends AbstractProcessor {
    function processItem(Item $item) {
        return new ProcessedWoodItem($item); //This has an inspection error
    }
}

class StoneProcessor extends AbstractProcessor {
    function processItem(Item $item) {
        return new ProcessedStoneItem($item);//This has an inspection error
    }
}

Так как я передаю только Itemto new ProcessedWoodItem($item)и он ожидает WoodItem в качестве параметра, проверка кода показывает, что произошла ошибка.

Danack
источник
1
Почему вы не используете интерфейсы? Я полагаю, что вы можете использовать наследование интерфейса для достижения желаемого.
Рибальд Эдди
Потому что два класса разделяют 80% своего кода, который находится в абстрактном классе. Использование интерфейсов означало бы либо дублирование кода, либо большой рефакторинг для перемещения общего кода в другой класс, который можно объединить с двумя классами.
Данак
Я уверен, что вы можете использовать оба вместе.
Рибальд Эдди
Да, но это не решает проблему, состоящую в том, что i) совместно используемым методам необходимо использовать базовый класс «Item». Ii) Методы, специфичные для типа, хотят использовать подкласс «WoodItem», но это выдает ошибку типа «Объявление». WoodProcessor :: bar () должен быть совместим с AbstractProcessor :: bar (Item $ item) "
Danack
У меня нет времени, чтобы на самом деле протестировать поведение, но что, если вы намекнули на интерфейс (создайте IItem и IWoodItem и наследуйте IWoodItem от IItem)? Затем подсказка IItem в сигнатуре функции для базового класса и IWoodItem в дочернем. Тат может работать. Может не.
RibaldEddie

Ответы:

3

Вы можете использовать методы без аргументов, документируя параметры с помощью doc-блоков:

<?php

class Foo
{
    /**
     * @param string $world
     */
    public function hello()
    {
        list($world) = func_get_args();

        echo "Hello, {$world}\n";
    }
}

class Bar extends Foo
{
    /**
     * @param string $greeting
     * @param string $world
     */
    public function hello()
    {
        list($greeting, $world) = func_get_args();

        echo "{$greeting}, {$world}\n";
    }
}

$foo = new Foo();
$foo->hello('World');

$bar = new Bar();
$bar->hello('Bonjour', 'World');

Я не собираюсь говорить, что думаю, что это хорошая идея.

Ваша проблема в том, что у вас есть контекст с переменным числом членов - вместо того, чтобы пытаться использовать их в качестве аргументов, лучшая и более перспективная идея состоит в том, чтобы ввести контекстный тип для переноса всех возможных аргументов, чтобы аргумент список никогда не нужно менять.

Вот так:

<?php

class HelloContext
{
    /** @var string */
    public $greeting;

    /** @var string */
    public $world;

    public static function create($world)
    {
        $context = new self;

        $context->world = $world;

        return $context;
    }

    public static function createWithGreeting($greeting, $world)
    {
        $context = new self;

        $context->greeting = $greeting;
        $context->world = $world;

        return $context;
    }
}

class Foo
{
    public function hello(HelloContext $context)
    {
        echo "Hello, {$context->world}\n";
    }
}

class Bar extends Foo
{
    public function hello(HelloContext $context)
    {
        echo "{$context->greeting}, {$context->world}\n";
    }
}

$foo = new Foo();
$foo->hello(HelloContext::create('World'));

$bar = new Bar();
$bar->hello(HelloContext::createWithGreeting('Bonjour', 'World'));

Статические фабричные методы, конечно, являются необязательными, но могут быть полезны, если только определенные конкретные комбинации элементов создают значимый контекст. Если это так, вы можете также объявить себя __construct()защищенным / частным.

mindplay.dk
источник
Обручи, которые нужно перепрыгнуть, чтобы симулировать ООП в PHP.
Тулаинс Кордова
4
@ user61852 Я не говорю об этом, чтобы защитить PHP (поверьте мне), но большинство языков не позволяло вам изменять унаследованную сигнатуру метода - исторически это было возможно в PHP, но по разным причинам решили (искусственно) ограничить программистов от этого, поскольку это вызывает фундаментальные проблемы с языком; это изменение было сделано, чтобы привести язык в большее соответствие с другими языками, поэтому я не уверен, с каким языком вы сравниваете. Шаблон, продемонстрированный во второй части моего ответа, применим и полезен и в других языках, таких как C # или Java.
mindplay.dk