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.