15

The below interface inheritance is illegal in PHP, but I think it would be fairly useful in real life. Is there an actual antipattern or documented problem with the below design, that PHP is protecting me from?

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

}
22

Let's ignore for a second that the method in question is __construct and call it frobnicate. Now suppose you have an object api implementing IHttpApi, and an object config implementing IHttpConfig. Clearly, this code fits the interface:

$api->frobnicate($config)

But let's suppose we upcast api to IApi, for example passing it to function frobnicateTwice(IApi $api). Now in that function, frobnicate is called, and since it only deals with IApi, it may perform a call such as $api->frobnicate(new SpecificConfig(...)) where SpecificConfig implements IConfig but not IHttpConfig. At no point anyone did anything unsavory with types, yet IHttpApi::frobnicate got a SpecificConfig where it expected a IHttpConfig.

This is no good. We don't want to prohibit upcasting, we want subtyping, and we clearly want multiple classes implementing an interface. So the only sensible option is to prohibit a subtype method requiring more specific types for parameters. (A similar problem occurs when you want to return a more general type.)

Formally, you've walked into a classic trap surrounding polymorphism, variance. Not all occurrences of a type T can be replaced by a subtype U. Conversely, not all occurrences of a type T can be replaces by a supertype S. Careful consideration (or better yet, strict application of type theory) is necessary.

Coming back to __construct: Since AFAIK you can't exactly instantiate an interface, only a concrete implementer, this may seem like a pointless restriction (it's never going to get called through an interface). But in that case, why include __construct in the interface to begin with? Regardless, it would be of little use to special-case __construct here.

19

Yes, this follows directly from the Liskov Substitution Principle (LSP). When you override a method, the return type can become more specific, while types of arguments must stay the same or can become more general.

This is more obvious with methods other than __construct. Consider:

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 CarDriver is a Driver, so a CarDriver instance must be able to do anything that a Driver can. Including driving Motorcycles, because it's just a Vehicle. But the argument type for drive says that a CarDriver can only drive Cars – a contradiction: CarDriver can't be a proper subclass of Driver.

The reverse makes more sense:

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

A CarDriver can only drive Cars. A MultiTalentedDriver can also drive Cars, because a Car is just a Vehicle. Therefore, MultiTalentedDriver is a proper subclass of CarDriver.

In your example, any IApi can be constructed with an IConfig. If IHttpApi is a subtype of IApi, we must be able to construct an IHttpApi using any IConfig instance – but it only accepts IHttpConfig. This is a contradiction.

  • Not all drivers can drive both cars and motorcycles... – sakisk Oct 14 '14 at 10:44
  • 3
    @faif: In this particular abstraction, they not only can, they must. Because, as you can see, a Driver can drive any Vehicle, and since both Car and Motorcycle extends Vehicle, all Drivers must be able to handle both. – Alex Oct 14 '14 at 10:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.