Illégal en PHP: y a-t-il une raison de conception OOP?

16

L'héritage d'interface ci-dessous est illégal en PHP, mais je pense qu'il serait assez utile dans la vie réelle. Existe-t-il un véritable problème de modèle ou documenté avec la conception ci-dessous, contre lequel PHP me protège?

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

}
kojiro
la source

Réponses:

22

Ignorons une seconde que la méthode en question est __constructet appelons-la frobnicate. Supposons maintenant que vous ayez un objet apiimplémenté IHttpApiet un objet configimplémenté IHttpConfig. De toute évidence, ce code correspond à l'interface:

$api->frobnicate($config)

Mais supposons que nous diffusions apivers IApi, par exemple en le passant à function frobnicateTwice(IApi $api). Maintenant, dans cette fonction, frobnicateest appelé, et puisqu'il ne traite que IApi, il peut effectuer un appel tel que $api->frobnicate(new SpecificConfig(...))where SpecificConfigimplements IConfigmais pas IHttpConfig. À aucun moment, personne n'a fait quoi que ce soit de désagréable avec les types, mais a IHttpApi::frobnicateobtenu un SpecificConfigendroit où il s'attendait à un IHttpConfig.

Ce n'est pas bien. Nous ne voulons pas interdire l'upcasting, nous voulons le sous-typage, et nous voulons clairement que plusieurs classes implémentent une interface. Ainsi, la seule option raisonnable est d'interdire une méthode de sous-type nécessitant des types plus spécifiques pour les paramètres. (Un problème similaire se produit lorsque vous souhaitez renvoyer un type plus général .)

Formellement, vous êtes entré dans un piège classique entourant le polymorphisme, la variance . Toutes les occurrences d'un type Tne peuvent pas être remplacées par un sous-type U. Inversement, toutes les occurrences d'un type Tne peuvent pas être remplacées par un supertype S . Un examen attentif (ou mieux encore, une application stricte de la théorie des types) est nécessaire.

Pour en revenir à __construct: depuis AFAIK, vous ne pouvez pas instancier exactement une interface, seulement un implémenteur concret, cela peut sembler une restriction inutile (il ne sera jamais appelé via une interface). Mais dans ce cas, pourquoi inclure __constructdans l'interface pour commencer? Quoi qu'il en soit, il serait de peu d'utilité dans ce cas particulier __construct.


la source
19

Oui, cela découle directement du principe de substitution de Liskov (LSP) . Lorsque vous remplacez une méthode, le type de retour peut devenir plus spécifique, tandis que les types d'arguments doivent rester les mêmes ou devenir plus généraux.

Cela est plus évident avec des méthodes autres que __construct. Considérer:

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

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

A CarDriverest un Driver, donc une CarDriverinstance doit être capable de faire tout ce qui est Driverpossible. Y compris la conduite Motorcycles, car c'est juste un Vehicle. Mais le type d'argument pour drivedit que a CarDriverne peut conduire que Cars - une contradiction: CarDriver ne peut pas être une sous-classe appropriée de Driver.

L'inverse est plus logique:

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

A CarDriverne peut conduire que Cars. A MultiTalentedDriverpeut également conduire Cars, car a Carest juste un Vehicle. Par conséquent, MultiTalentedDriverest une sous-classe appropriée de CarDriver.

Dans votre exemple, tout IApipeut être construit avec un IConfig. Si IHttpApiest un sous-type de IApi, nous devons pouvoir construire un en IHttpApiutilisant n'importe quelle IConfiginstance - mais il accepte seulement IHttpConfig. C'est une contradiction.

amon
la source
Tous les conducteurs ne peuvent pas conduire des voitures et des motos ...
sakisk
3
@faif: Dans cette abstraction particulière, non seulement ils le peuvent, ils le doivent. Parce que, comme vous pouvez le voir, un Driverpeut piloter n'importe lequel Vehicle, et puisque les deux Caret Motorcycles'étendent Vehicle, tous les Drivers doivent être capables de gérer les deux.
Alex