Nielegalne w PHP: czy istnieje powód do zaprojektowania OOP?

16

Poniższe dziedziczenie interfejsu jest nielegalne w PHP, ale myślę, że byłoby całkiem przydatne w prawdziwym życiu. Czy jest jakiś faktyczny lub udokumentowany problem z poniższym projektem, przed którym PHP mnie chroni?

<?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
źródło

Odpowiedzi:

22

Zignorujmy na chwilę tę metodę __constructi wywołaj ją frobnicate. Załóżmy teraz, że masz obiekt apiimplementujący IHttpApii obiekt configimplementujący IHttpConfig. Oczywiście ten kod pasuje do interfejsu:

$api->frobnicate($config)

Ale załóżmy, my uskok apido IApi, na przykład przekazując je function frobnicateTwice(IApi $api). Teraz w tej funkcji frobnicatejest wywoływany, a ponieważ zajmuje się tylko IApitym, może wykonywać wywołanie, takie jak $api->frobnicate(new SpecificConfig(...))gdzie SpecificConfigimplementuje, IConfigale nie IHttpConfig. W żadnym momencie nikt nie zrobił nic niesmacznego z typami, ale IHttpApi::frobnicatedotarł tam, SpecificConfiggdzie się spodziewał IHttpConfig.

To nie jest dobre. Nie chcemy zabraniać upcastingu, chcemy podsieci i wyraźnie chcemy, aby wiele klas implementowało interfejs. Zatem jedyną sensowną opcją jest zakazanie metody podtypu wymagającej bardziej szczegółowych typów parametrów. (Podobny problem występuje, gdy chcesz zwrócić bardziej ogólny typ.)

Formalnie wpadłeś w klasyczną pułapkę otaczającą polimorfizm, wariancję . Nie wszystkie wystąpienia danego typu Tmożna zastąpić podtypem U. I odwrotnie, nie wszystkie wystąpienia typu Tmożna zastąpić nadtypem S . Konieczne jest staranne rozważenie (lub jeszcze lepiej ścisłe zastosowanie teorii typów).

Wracając do __construct: Ponieważ AFAIK nie może dokładnie utworzyć instancji interfejsu, tylko konkretnego implementatora, może się to wydawać bezsensownym ograniczeniem (nigdy nie zostanie wywołane przez interfejs). Ale w takim przypadku, po co włączać __constructinterfejs? Niezależnie od tego, przydanie się __constructtutaj w specjalnym przypadku byłoby mało przydatne .


źródło
19

Tak, wynika to bezpośrednio z zasady substytucji Liskowa (LSP) . Po przesłonięciu metody typ zwracany może stać się bardziej szczegółowy, podczas gdy typy argumentów muszą pozostać takie same lub mogą stać się bardziej ogólne.

Jest to bardziej oczywiste w przypadku metod innych niż __construct. Rozważać:

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 CarDriverjest Driver, więc CarDriverinstancja musi być w stanie zrobić wszystko , co Drivermoże. Włącznie z jazdą Motorcycles, bo to tylko Vehicle. Ale typ argumentu dla drivemówi, że a CarDrivermoże prowadzić tylko Cars - sprzeczność: CarDriver nie może być właściwą podklasą Driver.

Rewers ma większy sens:

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

A CarDrivermoże prowadzić tylko Cars. A MultiTalentedDrivermoże także prowadzić Cars, ponieważ a Carjest po prostu a Vehicle. Dlatego MultiTalentedDriverjest właściwą podklasą CarDriver.

W twoim przykładzie dowolny IApimożna zbudować za pomocą IConfig. Jeśli IHttpApijest podtypem IApi, musimy być w stanie skonstruować IHttpApiużycie dowolnej IConfiginstancji - ale tylko akceptuje IHttpConfig. To jest sprzeczność.

amon
źródło
Nie wszyscy kierowcy mogą prowadzić zarówno samochody, jak i motocykle ...
sakisk
3
@faif: W tej szczególnej abstrakcji nie tylko mogą, muszą. Ponieważ, jak widać, A Drivermoże jeździć każdy Vehicle, a ponieważ oba Cari Motorcyclerozszerza Vehicle, wszystkie Drivers musi być w stanie obsługiwać oba.
Alex