Illegale in PHP: esiste un motivo di progettazione OOP?

15

L'ereditarietà dell'interfaccia di sotto è illegale in PHP, ma penso che sarebbe abbastanza utile nella vita reale. C'è un vero problema antipattern o documentato con il design sottostante, che mi sta proteggendo da PHP?

<?php

/**
 * Marker interface
 */
interface IConfig {}

/**
 * An api sdk tool
 */
interface IApi
{
    public __construct(IConfig $cfg);
}

/**
 * Api configuration specific to http
 */
interface IHttpConfig extends IConfig
{
    public getSomeNiceHttpSpecificFeature();
}

/**
 * Illegal, but would be really nice to have.
 * Is this not allowed by design?
 */
interface IHttpApi extends IApi
{
    /**
     * This constructor must have -exactly- the same
     * signature as IApi, even though its first argument
     * is a subtype of the parent interface's required
     * constructor parameter.
     */
    public __construct(IHttpConfig $cfg);

}
    
posta kojiro 13.10.2014 - 22:46
fonte

2 risposte

22

Ignoriamo per un secondo che il metodo in questione è __construct e chiamalo frobnicate . Supponiamo ora di avere un oggetto api che implementa IHttpApi e un oggetto config che implementa IHttpConfig . Chiaramente, questo codice si adatta all'interfaccia:

$api->frobnicate($config)

Ma supponiamo di raddoppiare api a IApi , ad esempio passandolo a function frobnicateTwice(IApi $api) . Ora in quella funzione, viene chiamato frobnicate , e poiché si occupa solo di IApi , può eseguire una chiamata come $api->frobnicate(new SpecificConfig(...)) dove SpecificConfig implementa IConfig ma non IHttpConfig . In nessun momento nessuno ha fatto nulla di sgradevole con i tipi, eppure IHttpApi::frobnicate ha ottenuto un SpecificConfig dove si aspettava un IHttpConfig .

Questo non va bene. Non vogliamo proibire l'upcasting, vogliamo sottotitolare e vogliamo chiaramente più classi che implementino un'interfaccia. Quindi l'unica opzione sensata è proibire un metodo di sottotipo che richiede tipi più specifici per i parametri. (Un problema simile si verifica quando si desidera restituire un altro tipo generale .)

Formalmente, sei entrato in una classica trappola che circonda il polimorfismo, varianza . Non tutte le occorrenze di un tipo T possono essere sostituite da un sottotipo U . Al contrario, non tutte le occorrenze di un tipo T possono essere sostituite da un supertype S . È necessaria un'attenta considerazione (o, meglio ancora, una rigorosa applicazione della teoria dei tipi).

Tornando a __construct : Poiché AFAIK non è possibile creare un'istanza esattamente come un'interfaccia, solo un implementatore concreto, può sembrare una limitazione inutile (non verrà mai chiamata attraverso un'interfaccia). Ma in tal caso, perché includere __construct nell'interfaccia per cominciare? Indipendentemente da ciò, sarebbe di scarsa utilità per caso speciale __construct qui.

    
risposta data 13.10.2014 - 23:18
fonte
18

Sì, questo segue direttamente dal Principio di sostituzione di Liskov (LSP) . Quando si esegue l'override di un metodo, il tipo restituito può diventare più specifico, mentre i tipi di argomenti devono rimanere uguali o diventare più generici.

Questo è più ovvio con metodi diversi da __construct . Prendere in considerazione:

class Vehicle {}
class Car extends Vehicle {}
class Motorcycle extends Vehicle {}

class Driver {
    public drive(Vehicle $v) { ... }
}
class CarDriver extends Driver {
    public drive(Car $c) { ... }
}

Un CarDriver è un Driver , quindi un'istanza CarDriver deve essere in grado di eseguire qualsiasi che un Driver possa. Compreso guidare Motorcycles , perché è solo un Vehicle . Ma il tipo di argomento per drive dice che un CarDriver può solo guidare Car s - una contraddizione: CarDriver non può essere una sottoclasse corretta di Driver .

Il contrario ha più senso:

class CarDriver {
    public drive(Car $c) { ... }
}
class MultiTalentedDriver extends CarDriver {
    public drive(Vehicle $v) { ... }
}

Un CarDriver può solo guidare Car s. Un MultiTalentedDriver può anche guidare Car s, perché un Car è solo un Vehicle . Pertanto, MultiTalentedDriver è una sottoclasse corretta di CarDriver .

Nel tuo esempio, qualsiasi IApi può essere costruita con un IConfig . Se IHttpApi è un sottotipo di IApi , dobbiamo essere in grado di costruire un IHttpApi utilizzando qualsiasi istanza di IConfig - ma accetta solo IHttpConfig . Questa è una contraddizione.

    
risposta data 13.10.2014 - 23:17
fonte

Leggi altre domande sui tag