Unzulässig in PHP: Gibt es einen OOP-Designgrund?

16

Die folgende Schnittstellenvererbung ist in PHP illegal, aber ich denke, dass sie im wirklichen Leben ziemlich nützlich wäre. Gibt es ein aktuelles Antimuster oder ein dokumentiertes Problem mit dem unten aufgeführten Design, vor dem PHP mich schützt?

<?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
quelle

Antworten:

22

Lassen Sie uns für eine Sekunde ignorieren, dass es sich um die betreffende Methode handelt, __constructund rufen Sie sie auf frobnicate. Angenommen, Sie haben ein Objekt apiimplementiert IHttpApiund ein Objekt configimplementiert IHttpConfig. Dieser Code passt eindeutig zur Benutzeroberfläche:

$api->frobnicate($config)

Aber nehmen wir apian IApi, wir sind verärgert , um es zum Beispiel weiterzugeben function frobnicateTwice(IApi $api). Nun wird in dieser Funktion frobnicateaufgerufen, und da es sich nur IApium eine Funktion handelt , kann es einen Aufruf ausführen, wie beispielsweise $api->frobnicate(new SpecificConfig(...))where SpecificConfigimplementiert, IConfigaber nicht IHttpConfig. Zu keinem Zeitpunkt tat jemand etwas Unangenehmes mit Typen, IHttpApi::frobnicatebekam aber einen, SpecificConfigwo er einen erwartete IHttpConfig.

Das ist nicht gut. Wir wollen Upcasting nicht verbieten, wir wollen Subtyping und wir wollen eindeutig mehrere Klassen, die eine Schnittstelle implementieren. Daher ist die einzig sinnvolle Option, eine Subtyp-Methode zu verbieten, die spezifischere Typen für Parameter erfordert . (Ein ähnliches Problem tritt auf, wenn Sie wollen zurückkehren einen allgemeinen Typ.)

Formal sind Sie in eine klassische Falle geraten, die sich mit Polymorphismus und Varianz befasst . Nicht alle Vorkommen eines Typs Tkönnen durch einen Untertyp ersetzt werden U. Umgekehrt können nicht alle Vorkommen eines Typs Tdurch einen Supertyp ersetzt werden S. Sorgfältige Überlegungen (oder noch besser strikte Anwendung der Typentheorie) sind erforderlich.

Zurück zu __construct: Da Sie mit AFAIK eine Schnittstelle nicht genau instanziieren können, sondern nur einen konkreten Implementierer, scheint dies eine sinnlose Einschränkung zu sein (sie wird niemals über eine Schnittstelle aufgerufen). Aber warum sollte man in diesem Fall zunächst __constructin die Benutzeroberfläche aufnehmen? Unabhängig davon wäre es hier von geringem Nutzen, Sonderfälle zu machen __construct.


quelle
19

Ja, dies folgt direkt aus dem Liskov Substitution Principle (LSP) . Wenn Sie eine Methode überschreiben, kann der Rückgabetyp spezifischer werden, während die Argumenttypen gleich bleiben müssen oder allgemeiner werden können.

Dies ist offensichtlicher bei anderen Methoden als __construct. Erwägen:

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 CarDriverist ein Driver, also CarDrivermuss eine Instanz in der Lage sein, alles zu tun, was ein Driverkann. Einschließlich Fahren Motorcycles, weil es nur ein Vehicle. Aber der Argumenttyp für drivebesagt, dass a CarDrivernur Cars antreiben kann - ein Widerspruch: CarDriver Kann keine richtige Unterklasse von sein Driver.

Das Gegenteil ist sinnvoller:

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

A CarDriverkann nur Cars fahren . A MultiTalentedDriverkann auch Cars fahren , weil a Carnur a ist Vehicle. Daher MultiTalentedDriverist eine richtige Unterklasse von CarDriver.

In Ihrem Beispiel kann jeder IApimit einem konstruiert werden IConfig. Wenn IHttpApies sich um einen Subtyp von handelt IApi, müssen wir in der Lage sein, eine IHttpApiusing- IConfigInstanz zu konstruieren - diese akzeptiert jedoch nur IHttpConfig. Das ist ein Widerspruch.

amon
quelle
Nicht alle Fahrer können sowohl Autos als auch Motorräder fahren ...
Sakisk
3
@faif: In dieser besonderen Abstraktion können sie nicht nur, sie müssen. Denn, wie Sie sehen, Driverkann ein jeder fahren Vehicle, und da beides Carund Motorcycleerweitert Vehicle, müssen alle Drivers in der Lage sein, beides zu bewältigen.
Alex