Zmiana sygnatury metody implementacji klas w PHP

9

Czy istnieje jakieś przyzwoite rozwiązanie problemu braku generycznych wersji PHP, które umożliwiają statyczną kontrolę kodu w celu wykrycia spójności typu?

Mam klasę abstrakcyjną, którą chcę podklasować, a także wymuszam zmianę jednej z metod z przyjmowania parametru jednego typu na przyjmowanie parametru, który jest podklasą tego parametru.

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

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

Nie jest to dozwolone w PHP, ponieważ zmienia podpis metod, co jest niedozwolone. Za pomocą ogólnych stylów Java możesz zrobić coś takiego:

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

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

Ale oczywiście PHP ich nie obsługuje.

Google w przypadku tego problemu, ludzie sugerują używanie instanceofdo sprawdzania błędów w czasie wykonywania, np

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");
        } 
    }
}

Ale to działa tylko w czasie wykonywania, nie pozwala na sprawdzenie kodu pod kątem błędów przy użyciu analizy statycznej - więc czy jest jakiś rozsądny sposób radzenia sobie z tym w PHP?

Bardziej kompletnym przykładem problemu jest:

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
    }
}

Ponieważ przekazuję tylko Itemdo new ProcessedWoodItem($item)i oczekuje on WoodItem jako parametru, kontrola kodu sugeruje, że wystąpił błąd.

Danack
źródło
1
Dlaczego nie korzystasz z interfejsów? Wierzę, że możesz użyć dziedziczenia interfejsu, aby osiągnąć to, czego chcesz.
RibaldEddie
Ponieważ dwie klasy dzielą 80% swojego kodu, który jest w klasie abstrakcyjnej. Korzystanie z interfejsów oznaczałoby albo duplikowanie kodu, albo duży refaktor, aby przenieść wspólny kod do innej klasy, która mogłaby zostać skomponowana z dwiema klasami.
Danack
Jestem pewien, że możesz używać obu razem.
RibaldEddie
Tak - ale nie rozwiązuje to problemu polegającego na tym, że i) metody współużytkowane muszą korzystać z podstawowej klasy „Element” ii) Metody specyficzne dla typu chcą korzystać z podklasy „WoodItem”, ale daje to błąd taki jak „Deklaracja WoodProcessor :: bar () musi być kompatybilny z AbstractProcessor :: bar (Item $ item) "
Danack
Nie mam czasu, aby przetestować zachowanie, ale co jeśli podpowiedziałeś interfejsowi (utwórz IItem i IWoodItem, a IWoodItem odziedziczy po IItem)? Następnie podpowiedź IItem w sygnaturze funkcji dla klasy podstawowej i IWoodItem w potomku. Tat może działać. Może nie.
RibaldEddie

Odpowiedzi:

3

Możesz użyć metod bez argumentów, zamiast tego dokumentując parametry za pomocą bloków 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');

Nie powiem jednak, że to dobry pomysł.

Twój problem polega na tym, że masz kontekst ze zmienną liczbą członków - zamiast próbować wymusić ich jako argumenty, lepszym i bardziej przyszłościowym pomysłem jest wprowadzenie typu kontekstowego, aby przenosić wszystkie możliwe argumenty, tak aby argument lista nigdy nie musi się zmieniać.

Tak jak:

<?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'));

Statyczne metody fabryczne są oczywiście opcjonalne - ale mogą być przydatne, jeśli tylko określone kombinacje elementów tworzą znaczący kontekst. Jeśli tak, możesz również zadeklarować, że jesteś __construct()chroniony / prywatny.

mindplay.dk
źródło
Obręcze, przez które trzeba przejść, aby zasymulować OOP w PHP.
Tulains Córdova
4
@ user61852 Nie mówię tego, aby bronić PHP (wierzcie mi), ale, ale większość języków nie pozwala na zmianę odziedziczonej sygnatury metody - historycznie było to możliwe w PHP, ale z różnych powodów postanowiono (sztucznie) ograniczyć programistów od zrobienia tego, ponieważ powoduje podstawowe problemy z językiem; ta zmiana została wprowadzona w celu dostosowania języka do innych języków, więc nie jestem pewien, z którym językiem się porównujesz. Wzorzec pokazany w drugiej części mojej odpowiedzi ma zastosowanie i jest użyteczny także w innych językach, takich jak C # lub Java.
mindplay.dk