Modifica della firma del metodo per l'implementazione delle classi in PHP


9

C'è qualche soluzione decente alla mancanza di generici di PHP che consenta all'ispezione del codice statico di rilevare la coerenza del tipo?

Ho una classe astratta, che voglio sottoclassare e far valere anche che uno dei metodi cambia dall'acquisizione di un parametro di un tipo, all'adozione di un parametro che è una sottoclasse di quel parametro.

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

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

Questo non è consentito in PHP perché sta cambiando la firma dei metodi che non è consentita. Con i generici in stile Java potresti fare qualcosa del tipo:

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

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

Ma ovviamente PHP non supporta quelli.

Google per questo problema, le persone suggeriscono di utilizzare instanceofper controllare gli errori in fase di esecuzione ad es

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

Ma funziona solo in fase di runtime, non ti consente di ispezionare il tuo codice per individuare errori utilizzando l'analisi statica - quindi esiste un modo ragionevole di gestirlo in PHP?

Un esempio più completo del problema è:

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

Poiché sto passando solo un Itemto new ProcessedWoodItem($item)e si aspetta un WoodItem come parametro, l'ispezione del codice suggerisce che c'è un errore.


1
Perché non usi le interfacce? Credo che tu possa usare l'ereditarietà dell'interfaccia per ottenere ciò che desideri.
RibaldEddie,

Perché le due classi condividono l'80% del loro codice, che è nella classe astratta. L'uso delle interfacce significherebbe duplicare il codice o un grande refactor per spostare il codice condiviso in un'altra classe che potrebbe essere composta con le due classi.
Danack,

Sono abbastanza sicuro che puoi usarli entrambi insieme.
RibaldEddie,

Sì, ma non risolve il problema che i) i metodi condivisi devono utilizzare la classe base di 'Item' ii) I metodi specifici del tipo vogliono usare una sottoclasse "WoodItem" ma che dà un errore come "Dichiarazione" di WoodProcessor :: bar () deve essere compatibile con AbstractProcessor :: bar (Item $ item) "
Danack

Non ho tempo per testare effettivamente il comportamento, ma cosa succede se hai suggerito l'interfaccia (creare un IItem e un IWoodItem e avere l'IWoodItem ereditare da IItem)? Quindi suggerisci IItem nella firma della funzione per la classe base e IWoodItem nel figlio. Tat potrebbe funzionare. Potrebbe no.
RibaldEddie,

Risposte:


3

Puoi usare metodi senza argomenti, documentando invece i parametri con doc-block:

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

Non ho intenzione di dire che penso che questa sia una buona idea.

Il tuo problema è che hai un contesto con un numero variabile di membri - piuttosto che cercare di forzarli come argomenti, un'idea migliore e più a prova di futuro è quella di introdurre un tipo di contesto per trasportare tutti i possibili argomenti, in modo che l'argomento l'elenco non deve mai cambiare.

Così:

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

I metodi statici di fabbrica sono ovviamente opzionali - ma potrebbero essere utili, se solo determinate combinazioni specifiche di membri producono un contesto significativo. In tal caso, potresti voler dichiarare __construct()anche protetto / privato.


I cerchi in cui si deve saltare per simulare OOP in PHP.
Tulains Córdova,

4
@ user61852 Non sto dicendo questo per difendere PHP (credimi) ma, ma la maggior parte delle lingue non ti permetterebbe di cambiare una firma del metodo ereditata - storicamente, questo era possibile in PHP, ma per vari motivi ha deciso di limitare (artificialmente) i programmatori dal fare ciò, poiché causa problemi fondamentali con la lingua; questa modifica è stata apportata per rendere la lingua più in linea con altre lingue, quindi non sono sicuro con quale lingua stai confrontando. Il modello dimostrato nella seconda parte della mia risposta è applicabile e utile anche in altre lingue come C # o Java.
mindplay.dk,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.